Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    autocreate RS node material based texture sets in tex folder

    Cinema 4D SDK
    2025 python windows
    2
    9
    1.1k
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • A
      apetownart
      last edited by

      Hi in advanced im sorry if im asking for help with something the forumn is not used for, im just tryign to help out a buddies pipeline and im by no means the best with the maxon api documentation. So far my script lets me auto make materials based on texture sets in the texture folder. any help to understand the error so i cna implement a fix would be super helpfula and by no means expected. thanks in advanced.

      import c4d
      import maxon
      import os
      import re
      
      # Redshift Constants
      ID_REDSHIFT_ENGINE = 1036219
      ID_REDSHIFT_NODESPACE = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace")
      ID_NODE_EDITOR_MODE_MATERIAL = 465002360
      
      # Texture Naming Pattern (Assumes: mesh_textureSet_mapType.png)
      TEXTURE_NAME_PATTERN = re.compile(r"(.+?)_(.+?)_.+\.(png|jpg|tif|exr)")
      
      def get_texture_sets(tex_folder):
          """Extracts unique texture set names from files in the 'tex' folder."""
          texture_sets = set()
      
          if not os.path.exists(tex_folder):
              return texture_sets
      
          for file in os.listdir(tex_folder):
              match = TEXTURE_NAME_PATTERN.match(file)
              if match:
                  mesh, texture_set = match.groups()[:2]
                  texture_sets.add(f"{mesh}_{texture_set}")
      
          return texture_sets
      
      def create_redshift_material(name):
          """Creates a Redshift Node Material with the given name."""
          doc = c4d.documents.GetActiveDocument()
      
          # Set the Node Editor to Material Mode
          if not c4d.IsCommandChecked(ID_NODE_EDITOR_MODE_MATERIAL):
              c4d.CallCommand(ID_NODE_EDITOR_MODE_MATERIAL)
      
          # Ensure Redshift is the active render engine
          render_settings = doc.GetActiveRenderData()
          if render_settings:
              render_settings[c4d.RDATA_RENDERENGINE] = ID_REDSHIFT_ENGINE
      
          # Create a new Redshift Material
          c4d.CallCommand(1036759)  # Redshift Material
          c4d.EventAdd()
      
          # Get the newly created material
          material = doc.GetActiveMaterial()
          if not material:
              raise RuntimeError("Failed to create Redshift Material.")
      
          material.SetName(name)
      
          # Get Node Material Reference
          node_material = material.GetNodeMaterialReference()
          if not node_material:
              raise RuntimeError("Failed to retrieve the Node Material reference.")
      
          # Ensure the material is in the Redshift Node Space
          if not node_material.HasSpace(ID_REDSHIFT_NODESPACE):
              graph = node_material.AddGraph(ID_REDSHIFT_NODESPACE)
          else:
              graph = node_material.GetGraph(ID_REDSHIFT_NODESPACE)
      
          if graph is None:
              raise RuntimeError("Failed to retrieve or create the Redshift Node Graph.")
      
          # Update and refresh Cinema 4D
          material.Message(c4d.MSG_UPDATE)
          c4d.EventAdd()
          
          print(f" Created Redshift Node Material: {name}")
      
          return material
      
      def main():
          """Main function to create Redshift materials based on texture sets in 'tex' folder."""
          doc = c4d.documents.GetActiveDocument()
          tex_folder = os.path.join(doc.GetDocumentPath(), "tex")
      
          texture_sets = get_texture_sets(tex_folder)
          if not texture_sets:
              c4d.gui.MessageDialog("No texture sets found in 'tex' folder!")
              return
      
          for texture_set in texture_sets:
              create_redshift_material(texture_set)
      
      # Execute the script
      if __name__ == '__main__':
          main()
      
      

      This works great so far. the issues im running into is when i try and auto assign texture nodes in the node graph and assign them to the material inputs

      import c4d
      import maxon
      import os
      import re
      
      # Constants
      ID_REDSHIFT_ENGINE = 1036219
      ID_NODE_EDITOR_MODE_MATERIAL = 465002360
      TEXTURE_NAME_PATTERN = re.compile(r"(.+?)_(.+?)_.+\.(png|jpg|tif|exr)")
      
      # Mapping texture types to their corresponding Redshift shader node IDs
      TEXTURE_TYPES = {
          "BaseColor": "com.redshift3d.redshift4c4d.nodes.core.texturesampler",
          "Normal": "com.redshift3d.redshift4c4d.nodes.core.bumpmap",
          "Roughness": "com.redshift3d.redshift4c4d.nodes.core.texturesampler",
          "Metalness": "com.redshift3d.redshift4c4d.nodes.core.texturesampler",
          "Opacity": "com.redshift3d.redshift4c4d.nodes.core.texturesampler",
          "Displacement": "com.redshift3d.redshift4c4d.nodes.core.displacement",
          "EmissionColor": "com.redshift3d.redshift4c4d.nodes.core.texturesampler"
      }
      
      def get_texture_sets(tex_folder):
          """Extracts unique texture set names and maps available textures."""
          texture_sets = {}
      
          if not os.path.exists(tex_folder):
              return texture_sets
      
          for file in os.listdir(tex_folder):
              match = TEXTURE_NAME_PATTERN.match(file)
              if match:
                  mesh, texture_set, _ = match.groups()
                  key = f"{mesh}_{texture_set}"
                  if key not in texture_sets:
                      texture_sets[key] = {}
                  for channel in TEXTURE_TYPES.keys():
                      if channel.lower() in file.lower():
                          texture_sets[key][channel] = os.path.join(tex_folder, file)
      
          return texture_sets
      
      def create_redshift_material(name, texture_files):
          """Creates a Redshift Node Material and assigns textures."""
          doc = c4d.documents.GetActiveDocument()
      
          # Set the Node Editor to Material Mode
          if not c4d.IsCommandChecked(ID_NODE_EDITOR_MODE_MATERIAL):
              c4d.CallCommand(ID_NODE_EDITOR_MODE_MATERIAL)
      
          # Ensure Redshift is the active render engine
          render_settings = doc.GetActiveRenderData()
          if render_settings:
              render_settings[c4d.RDATA_RENDERENGINE] = ID_REDSHIFT_ENGINE
      
          # Create a new Redshift Material
          c4d.CallCommand(1036759)  # Redshift Material
          c4d.EventAdd()
      
          # Get the newly created material
          material = doc.GetActiveMaterial()
          if not material:
              raise RuntimeError("Failed to create Redshift Material.")
      
          material.SetName(name)
      
          # Get Node Material Reference
          node_material = material.GetNodeMaterialReference()
          if not node_material:
              raise RuntimeError("Failed to retrieve the Node Material reference.")
      
          # Ensure the material is in the Redshift Node Space
          redshift_nodespace_id = maxon.NodeSpaceIdentifiers.RedshiftMaterial
          if not node_material.HasSpace(redshift_nodespace_id):
              graph = node_material.AddGraph(redshift_nodespace_id)
          else:
              graph = node_material.GetGraph(redshift_nodespace_id)
      
          if graph is None:
              raise RuntimeError("Failed to retrieve or create the Redshift Node Graph.")
      
          print(f" Created Redshift Node Material: {name}")
      
          # Create Texture Nodes and Connect Them
          with maxon.GraphTransaction(graph) as transaction:
              texture_nodes = {}
              for channel, file_path in texture_files.items():
                  print(f"🔹 Creating node for {channel}: {file_path}")
      
                  # Create the texture node
                  node_id = maxon.Id(TEXTURE_TYPES[channel])
                  node = graph.AddChild("", node_id, maxon.DataDictionary())
                  if node is None:
                      print(f" Failed to create node for {channel}")
                      continue
      
                  # Set the filename parameter
                  filename_port = node.GetInputs().FindChild("filename")
                  if filename_port:
                      filename_port.SetDefaultValue(maxon.Url(file_path))
                  else:
                      print(f" 'filename' port not found for {channel} node.")
      
                  texture_nodes[channel] = node
                  print(f" Successfully created {channel} texture node.")
      
              # Connect Texture Nodes to Material Inputs
              material_node = graph.GetRoot()
              if material_node:
                  for channel, tex_node in texture_nodes.items():
                      input_port_id = f"{channel.lower()}_input"  # Example: 'basecolor_input'
                      material_input_port = material_node.GetInputs().FindChild(input_port_id)
                      if material_input_port:
                          tex_output_port = tex_node.GetOutputs().FindChild("output")
                          if tex_output_port:
                              material_input_port.Connect(tex_output_port)
                              print(f"🔗 Connected {channel} texture to material.")
                          else:
                              print(f" 'output' port not found on {channel} texture node.")
                      else:
                          print(f" '{input_port_id}' port not found on material node.")
              else:
                  print(" Material node (root) not found in the graph.")
      
              transaction.Commit()
      
          # Update and refresh Cinema 4D
          material.Message(c4d.MSG_UPDATE)
          c4d.EventAdd()
      
          return material
      
      def main():
          """Main function to create Redshift materials based on texture sets in 'tex' folder."""
          doc = c4d.documents.GetActiveDocument()
          tex_folder = os.path.join(doc.GetDocumentPath(), "tex")
      
          texture_sets = get_texture_sets(tex_folder)
          if not texture_sets:
              c4d.gui.MessageDialog("No texture sets found in 'tex' folder!")
              return
      
          for texture_set, texture_files in texture_sets.items():
              create_redshift_material(texture_set, texture_files)
      
      # Execute the script
      if __name__ == '__main__':
          main()
      
      

      The error im geting if it does nto just crash outright 🙂

      Traceback (most recent call last):
       line 147, in <module>
       line 143, in main
       line 84, in create_redshift_material
       line 295, in __init__
       line 164, in __init__
          raise TypeError("passed object is not of same type to create copy")
      TypeError: passed object is not of same type to create copy```
      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @apetownart
        last edited by ferdinand

        Hello @apetownart,

        Thank you for reaching out to us. Do you have an example texture set we could use? Without it, it is a bit hard to have a look at your code example, due to the pattern "(.+?)_(.+?)_.+\.(png|jpg|tif|exr)" you use. Graph descriptions could also be an alternative for you. You could at least use GraphDescription.GetGraph to avoid some boiler plate code when you otherwise prefer approaching this more manually.

        Cheers,
        Ferdinand

        MAXON SDK Specialist
        developers.maxon.net

        1 Reply Last reply Reply Quote 0
        • A
          apetownart
          last edited by

          @ferdinand

          thank you for the reply. its looking at texture sets from SUbstance painter. here are some examples.

          tex.zip

          ferdinandF 1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand @apetownart
            last edited by ferdinand

            Hello @apetownart,

            so, I had a look at your code, and there are unfortunately quite a few issues with it. Did you write this code yourself? We have a code example which is quite close to what you are trying to do. Your code strikes me as AI-generated, but I might be wrong.

            Please understand that we cannot provide support on this level of issues, we expect users to generally follow our documentation, we cannot provide here line-by-line support. I have attached below your script in a commented version up to the point to which I debugged it. What you want to do, is certainly possible with the low level Nodes API in Python.

            I would recommend to use the more artist friendly high level graph description API in your case. I have attached an example below.

            ⚠ maxon.GraphDescription.ApplyDescription currently prints junk to the console when executed. This is a known issue and will be fixed in the next release.

            Cheers,
            Ferdinand

            Result

            ba71a93d-78da-4bb9-968d-c457834e563b-image.png

            Code

            """Demonstrates how to compose materials using graph descriptions for texture bundles provided
            by a texture website or an app like substance painter.
            
            The goal is to construct materials over the regular naming patterns such texture packs exhibit.
            """
            import os
            
            import c4d
            import maxon
            import mxutils
            
            doc: c4d.documents.BaseDocument # The active document.
            
            # The texture formats we support loading, could be extended to support more formats.
            TEXTURE_FORMATS: tuple[str] = (".png", ".jpg", ".tif", ".exr")
            
            # Texture postfix classification data. This map exists so that you could for example map both the
            # keys "color" and "diffuse" to the same internal classification "color". I did not make use of this
            # multi routing here.
            TEXTURE_CLASSIFICATIONS: dict = {
                "color": "color",
                "roughness": "roughness",
                "normal": "normal",
            }
            
            def GetTextureData(doc: c4d.documents.BaseDocument) -> dict[str, dict[str, str]]:
                """Ingests the texture data from the 'tex' folder of the passed document and returns it as a
                dictionary.
            
                This is boilerplate code that is effectively irrelevant for the subject of using graph
                descriptions to construct materials.
                """
                # Get the document's texture folder path.
                path: str = mxutils.CheckType(doc).GetDocumentPath()
                if not path:
                    raise RuntimeError("Cannot operate on an unsaved document.")
            
                texFolder: str = os.path.join(path, "tex")
                if not os.path.exists(texFolder):
                    raise RuntimeError(f"Texture folder '{texFolder}' does not exist.")
            
                # Start building texture data.
                data: dict[str, dict[str, str]] = {}
                for file in os.listdir(texFolder):
                    # Fiddle paths, formats, and the classification label into place.
                    fPath: str = os.path.join(texFolder, file)
                    if not os.path.isfile(fPath):
                        continue
            
                    name: str = os.path.splitext(file)[0].lower()
                    ext: str = os.path.splitext(file)[1].lower()
                    if ext not in TEXTURE_FORMATS:
                        continue
            
                    items: list[str] = name.rsplit("_", 1)
                    if len(items) != 2:
                        raise RuntimeError(
                            f"Texture file '{file}' is not following the expected naming convention.")
            
                    # Get the label, i.e., unique part of the texture name, and its classification postfix.
                    label: str = items[0].lower()
                    classification: str = items[1].lower()
                    if classification not in TEXTURE_CLASSIFICATIONS.keys():
                        continue
                    
                    # Map the classification to its internal representation and write the data.
                    container: dict[str, str] = data.get(label, {})
                    container[TEXTURE_CLASSIFICATIONS[classification]] = fPath
                    data[label] = container
                    
                return data
            
            def CreateMaterials(doc: c4d.documents.BaseDocument) -> None:
                """Creates Redshift materials based on the texture data found in the 'tex' folder of the passed
                document.
                """
                # Get the texture data for the document.
                mxutils.CheckType(doc)
                data: dict[str, dict[str, str]] = GetTextureData(doc)
                 
                # Iterate over the texture data and create materials.
                for label, container in data.items():
                    # Create a Redshift Material and get its graph.
                    graph: maxon.NodesGraphModelRef = maxon.GraphDescription.GetGraph(
                        name=label, nodeSpaceId=maxon.NodeSpaceIdentifiers.RedshiftMaterial)
            
                    # We could have also created a graph with the default nodes in it in the call above, but
                    # it is just easier to do things manually here. So, we create an empty graph and add here
                    # an output node with a standard material attached. See
                    #
                    #   https://developers.maxon.net/docs/py/2025_1_0/manuals/manual_graphdescription.html
                    #
                    # for more information on how to use the graph description API.
                    description: dict = {
                        "$type": "Output",
                        "Surface": {
                            "$type": "Standard Material",
                        }
                    }
            
                    # Extend the description based on the textures we found in the current texture pack.
            
                    # Attach a texture node with the color texture to the "Base/Color" port of the Standard
                    # Material node.
                    if "color" in container:
                        description["Surface"]["Base/Color"] = {
                            "$type": "Texture",
                            "General/Image/Filename/Path": maxon.Url(f"file:///{container['color']}")
                        }
                    # The same for roughness.
                    if "roughness" in container:
                        description["Surface"]["Base/Diffuse Roughness"] = {
                            "$type": "Texture",
                            "General/Image/Filename/Path": maxon.Url(f"file:///{container['roughness']}")
                        }
                    # Basically the same again, just that we also add a "Bump Map" node when we link a
                    # normal map.
                    if "normal" in container:
                        description["Surface"]["Geometry/Bump Map"] = {
                        "$type": "Bump Map",
                        "Input Map Type": 1, # Tangent Space
                        "Height Scale": .2, # Adjust this value to match the normal map intensity.
                        "Input": {
                            "$type": "Texture",
                            "Image/Filename/Path": maxon.Url(f"file:///{container['normal']}")
                        }
                    }
            
                    # And finally apply it to the graph.
                    maxon.GraphDescription.ApplyDescription(graph, description)
            
            # Execute the script
            if __name__ == '__main__':
                CreateMaterials(doc)
                c4d.EventAdd()
            

            Your Code

            def create_redshift_material(name, texture_files):
                """Creates a Redshift Node Material and assigns textures."""
                doc = c4d.documents.GetActiveDocument()
            
                # (FH): Not necessary and counter productive.
                if not c4d.IsCommandChecked(ID_NODE_EDITOR_MODE_MATERIAL):
                    c4d.CallCommand(ID_NODE_EDITOR_MODE_MATERIAL)
            
                # (FH): That is not sufficient, see code examples. You also have to add the post effect.
                # Ensure Redshift is the active render engine.
                render_settings = doc.GetActiveRenderData()
                if render_settings:
                    render_settings[c4d.RDATA_RENDERENGINE] = ID_REDSHIFT_ENGINE
            
                # (FH): Avoid commands, use the API instead. Invoking events multiple times in scripts is also
                # pointless as a script manager script is blocking. I.e., having a loop which changes the scene
                # and calls EventAdd() on each iteration, or the same loop and calling EventAdd() once at the
                # end of the script will have the same effect.
                c4d.CallCommand(1036759)  # Redshift Material
                c4d.EventAdd()
            
                # (FH): Could be condensed to a single line using maxon.GraphDescription.GetGraph()
                material = doc.GetActiveMaterial()
                if not material:
                    raise RuntimeError("Failed to create Redshift Material.")
            
                material.SetName(name)
                node_material = material.GetNodeMaterialReference()
                if not node_material:
                    raise RuntimeError("Failed to retrieve the Node Material reference.")
                redshift_nodespace_id = maxon.NodeSpaceIdentifiers.RedshiftMaterial
                if not node_material.HasSpace(redshift_nodespace_id):
                    graph = node_material.AddGraph(redshift_nodespace_id)
                else:
                    graph = node_material.GetGraph(redshift_nodespace_id)
            
                if graph is None:
                    raise RuntimeError("Failed to retrieve or create the Redshift Node Graph.")
            
                print(f" Created Redshift Node Material: {name}")
            
                # (FH) Incorrect call and the cause for your crashes.
                # with maxon.GraphTransaction(graph) as transaction:
                with graph.BeginTransaction() as transaction:
                    texture_nodes = {}
                    for channel, file_path in texture_files.items():
                        # (FH) Avoid using such exotic Unicode symbols, Python supports them but not every 
                        # system in Cinema 4D does.
                        print(f"🔹 Creating node for {channel}: {file_path}")
            
                        node_id = maxon.Id(TEXTURE_TYPES[channel])
                        node = graph.AddChild("", node_id, maxon.DataDictionary())
                        if node is None:
                            print(f" Failed to create node for {channel}")
                            continue
            
                        # (FH) That cannot work, #filename is not a port which is a direct child of a RS 
                        # texture node, but a child port of the "tex0" port bundle, the port is also called
                        # "path" and not "filename". 
            
                        # See: scripts/05_modules/node/create_redshift_nodematerial_2024.py in
                        # https://github.com/Maxon-Computer/Cinema-4D-Python-API-Examples/ for an example for
                        # how to construct a Redshift Node Material with Python, likely covering most what you
                        # need to know.
                        filename_port = node.GetInputs().FindChild("filename")
                        if filename_port:
                            # Will fail because filename_port is not a valid port.
                            filename_port.SetDefaultValue(maxon.Url(file_path))
                        else:
                            print(f" 'filename' port not found for {channel} node.")
            
            # (FH): Stopped debugging from here on out ...
            
                        texture_nodes[channel] = node
                        print(f" Successfully created {channel} texture node.")
            
                    material_node = graph.GetRoot()
                    if material_node:
                        for channel, tex_node in texture_nodes.items():
                            input_port_id = f"{channel.lower()}_input"
                            material_input_port = material_node.GetInputs().FindChild(input_port_id)
                            if material_input_port:
                                tex_output_port = tex_node.GetOutputs().FindChild("output")
                                if tex_output_port:
                                    material_input_port.Connect(tex_output_port)
                                    print(f"🔗 Connected {channel} texture to material.")
                                else:
                                    print(f" 'output' port not found on {channel} texture node.")
                            else:
                                print(f" '{input_port_id}' port not found on material node.")
                    else:
                        print(" Material node (root) not found in the graph.")
            
                    transaction.Commit()
            
                return material
            
            def main():
                """Main function to create Redshift materials based on texture sets in 'tex' folder."""
                # (FH) Not necessary, #doc is already available in the global scope.
                doc = c4d.documents.GetActiveDocument()
                tex_folder = os.path.join(doc.GetDocumentPath(), "tex")
            
                texture_sets = get_texture_sets(tex_folder)
                if not texture_sets:
                    c4d.gui.MessageDialog("No texture sets found in 'tex' folder!")
                    return
            
                for texture_set, texture_files in texture_sets.items():
                    create_redshift_material(texture_set, texture_files)
            

            MAXON SDK Specialist
            developers.maxon.net

            1 Reply Last reply Reply Quote 0
            • A
              apetownart
              last edited by

              thanks for the help. I will adjust it and fix it.

              1 Reply Last reply Reply Quote 0
              • A
                apetownart
                last edited by

                I was getting super explicit with node space calls because i was running into api issues where the consoole would have problems finding the right node space so i was trying to fix that originally. Most of the Python as a backend dev so i was usingertign it more generic as opposed to using API functions.

                1 Reply Last reply Reply Quote 0
                • A
                  apetownart
                  last edited by

                  @ferdinand I probably know the answer to this before i ask but ill ask anyway. is there a version agnostic way to call the node space so the script runs ok in 2024 or will i need to use a method not depricated.

                  1 Reply Last reply Reply Quote 0
                  • ferdinandF
                    ferdinand
                    last edited by ferdinand

                    Hi,

                    I am not quite sure how you mean this, what do you consider to be a 'version agnostic way to call the node space'? Do you mean if there are node space ID symbols exposed in earlier versions than 2025? Unfortunately not. But maxon.NodeSpaceIdentifiers is actually one of the very few things we define in Python (in resource\modules\python\libs\python311\maxon\frameworks\nodes.py), so you could just monkey patch earlier versions.

                    import maxon
                    
                    # Copied from `nodes.py`, I just made the namespaces explicit.
                    class NodeSpaceIdentifiers:
                        """ 
                        Holds the identifiers for all node spaces shipped with Cinema 4D.
                    
                        .. note::
                    
                            When you want to reference the node space added by a plugin, you must ask the plugin vendor for the node space
                            id literal of that plugin, e.g., `com.fancyplugins.nodespace.foo`. When asked for a node space ID in the API,
                            you must then pass `Id("com.fancyplugins.nodespace.foo")`.
                        """
                        #: References the node space used by node materials for the Standard renderer.
                        StandardMaterial: maxon.Id = maxon.Id("net.maxon.nodespace.standard")
                    
                        #: References the node space used by node materials for the Redshift renderer.
                        RedshiftMaterial: maxon.Id = maxon.Id("com.redshift3d.redshift4c4d.class.nodespace")
                        
                        #: References the node space used by Scene Nodes graphs.
                        SceneNodes: maxon.Id = maxon.Id("net.maxon.neutron.nodespace")
                    
                    # Attache the class to `maxon` under `NodeSpaceIdentifiers` when it is not there. You could also
                    # add a version check to be extra sure.
                    if not hasattr(maxon, "NodeSpaceIdentifiers"):
                        maxon.NodeSpaceIdentifiers = NodeSpaceIdentifiers
                        
                    
                    print (maxon.NodeSpaceIdentifiers.SceneNodes)
                    

                    edit: NodeSpaceIdentifiers has actually been introduced with 2024.3

                    MAXON SDK Specialist
                    developers.maxon.net

                    1 Reply Last reply Reply Quote 0
                    • A
                      apetownart
                      last edited by

                      thanks @ferdinand this makes perfect sense now. I appreciate the time you took to explaint he best practice.

                      1 Reply Last reply Reply Quote 0
                      • First post
                        Last post