Python Generator - Linking ports to xpresso node



  • Hi all.

    I’m wanting to use a python generator to create an xpresso tag that then links user data to an object’s attribute.

    I’ve got as far as being able to add the xpresso tag and have added the object node in the xpresso tag, but I’m having no joy with adding ports and linking them.

    Could anyone show me a simple example that does this?

    Many thanks,
    Jamie



  • Hi,

    I am not quite sure if I do understand your question correctly, but I assume you want to to know how to connect two c4d.modules.graphview.GvPorts?

    """An example for creating and connecting graphview nodes and ports. The
     script is meant to be run form the Script Manager and has no prerequisites.
    """
    
    import c4d
    
    def main():
        """
        """
        # Create a null object and add an Xpresso tag to it.
        null = c4d.BaseList2D(c4d.Onull)
        xpresso_tag = c4d.BaseList2D(c4d.Texpresso)
        null.InsertTag(xpresso_tag)
        
        # Get the node master and the root noode of that graphview.
        master = xpresso_tag.GetNodeMaster()
        root = master.GetRoot()
    
        # Create and add a math and constant node to that graphview
        const_node = master.CreateNode(root, c4d.ID_OPERATOR_CONST, x=10, y=10)
        math_node = master.CreateNode(root, c4d.ID_OPERATOR_MATH,  x=200, y=10)
        
        # Get all out ports of the constant node and select the first one.
        const_node_out = const_node.GetOutPorts()
        const_node_out = const_node_out[0] if const_node_out else None
    
        # Get all in ports of the math node and select the first one.
        math_node_in = math_node.GetInPorts()
        math_node_in = math_node_in[0] if math_node_in else None
        
        # Connect the constant node to the math node.
        if const_node_out and math_node_in:
            const_node_out.Connect(math_node_in)
    
        # Add our object to the scene graph and let Cinema update.
        doc.InsertObject(null)
        c4d.EventAdd()
        
    if __name__=='__main__':
        main()
    

    Cheers,
    zipit



  • Thanks @zipit - that's helped me get a better understanding for connecting ports (please forgive me, I'm still very new to python). However, I'm still stuck with trying to add a 'User Data' port to the node.

    The Node I have created is c4d.ID_OPERATOR_OBJECT and then I use the following code to make sure it's referring to the right object:

    parentNode[c4d.GV_OBJECT_PATH_TYPE] = 0
    parentNode[c4d.GV_OBJECT_START_TYPE_ID] = 1
    

    But then I try and use the AddPort function to add an output port to the node but I can't get it working.



  • Hi,

    ah, okay now I do understand. Unfortunately GvNode.AddPort has been broken for years now and apparently has not been fixed yet.

    See here: https://plugincafe.maxon.net/topic/11423/gv-node-addport-fail/3

    edit: Whoops, wrong link.

    Cheers,
    zipit



  • Thanks again @zipit

    From the link you sent it looks like it's fixed for R21. So I'm downloading the trial version now to see if I can get it working.



  • Hi all. I've installed the trial version of R21 (I'm looking to upgrade at some point anyway). However, I'm still struggling to get some python code that successfully adds ports to a GV node and then connects them.

    Any help would be massively appreciated.

    Jamie



  • Hi again ;)

    You should provide your code, as it would make it easier to help you.

    1. What is the return value of your AddPort call?
    2. What does a matching call to AddPortIsOK return for your arguments?
    3. Did you follow the special way in which a user data DescID has to be constructed, in order to be recognised by this method?

    Cheers,
    zipit



  • Thanks again @zipit

    To make things a bit clearer, here is what I am trying to do:

    Use a Python Generator to:

    • Create a Connector object
    • Create an Xpresso tag and assign it to the Connector object
    • Create a Deformer object (eg Bend) that is a child of the Connector object
    • Create a Node in the GV for the Connector Object
    • Create output ports for all of the Connector object's User Data.
    • Connect these ports to attributes of the Deformer object.

    I'm afraid I've got so confused with trying to get this right, that my code has become really messy with lots of things commented out as I try to figure what is working and what isn't.

    So I've created a new Python Generator with this code which is pretty much where I'm up to:

    def main():
        # Create Connector Object and insert an Xpresso tag to it
        connector_obj      = c4d.BaseObject(c4d.Oconnector)
        tag_xpresso        = c4d.BaseTag(c4d.Texpresso)
        connector_obj.InsertTag(tag_xpresso)
    
        # Get the node master and the root noode of that graphview.
        gvNode_master      = tag_xpresso.GetNodeMaster()
        gvNode_root        = gvNode_master.GetRoot()
        
        # Create a GV Node in the node Master 
        gvNode_controller  = gvNode_master.CreateNode(gvNode_root, c4d.ID_OPERATOR_OBJECT) 
        
        # Add port to GV Node
        USERDATA_NUMBER = 1
        gvNode_controller.AddPort(c4d.GV_PORT_OUTPUT, c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA, c4d.DTYPE_SUBCONTAINER, 0), c4d.DescLevel(USERDATA_NUMBER)), message=True)
        
        return connector_obj
    


  • Hi,

    I should have probably red your postings more carefully, but I am just now seeing that you are trying to do that in a generator object.

    A python generator object's main function is very similar to the GetVirtualObjects method in an ObjectData plugin, as it is being polled by Cinema to build the cache of your object. Cache means here that Cinema will not evaluate the content of your returned node hierarchy, which also means that it won't evaluate any expressions, including your Xpresso tag. So your approach is generally flawed unless you are not actually interested in the Xpresso graph updating your returned node hierarchy, but only interested in providing the user with a "ready to go" rig when converting your generator object into its cache representation via Current State to Object.

    Aside form that:

    1. Your code looks okay.
    2. Print out the return value of your GvNode.AddPort call, it should be a GvPort or None when something went wrong.
    3. Use GvNode.AddPortIsOK to test if Cinema thinks that adding a port here is okay. It should return True.

    Also noteworthy: The bug regarding GvNode.AddPort was that it always returned None and did not add any port, while a matching call to GvNode.AddPortIsOK would return True. The bug is really, really, ..., really old, there was a myriad of threads on it on the old forum. It also has been reported as fixed before and then either broke again or actually had not been fixed if my memory serves me right. So I would not be to shocked if it is actually still broken.

    Cheers,
    zipit



  • @zipit Ah I did wonder if the behaviour was slightly different due to it being a Python Generator.

    I think I understand most of what you have said there, but being new to Python (and coding really) it can be quite tricky for me grasp. From the sounds of it, I might be trying to do TOO MUCH of it within a Python Generator? I'm open to better/alternative pipelines.

    Here is a bit more detail about what I'm trying to to achieve overall:

    • Create a Python Generator that:
      • Has a series of User Data values which it then uses to create a hierarchy with a Connect Object at the top (root) (tick)
      • When collapsing the Python Generator by pressing 'C', the Connect object also inherits the same User Data from the Python Generator (tick - it does this by default)
      • The Connect object also has an xpresso tag that links certain attributes within the hierarchy to the Connect object's User Data. (this is where I am stuck)

    So overall, what I would like to have is a Python Generator which allows me to change certain User Data values so that it generates the correct hierarchy of Cloners, Objects and Deformers. (This all works fine) , but then what I would also like is the Python Generator to create an Xpresso tag that dynamically links certain attributes to the root object's User Data so that even after I have collapsed the Python Generator into the hierarchy, I can still change the User Data on the root object (The Connect object) and Deformer objects within the hierarchy will change accordingly.

    I hope that makes sense?

    Does this sound like something that is possible?

    Cheers,
    Jamie



  • @moGRR said:

    but then what I would also like is the Python Generator to create an Xpresso tag that dynamically links certain attributes to the root object's User Data so that even after I have collapsed the Python Generator into the hierarchy ...

    I assume with root object you are referring to the object returned by your main function and not the generator object (which is kind of the root of your cache). Technically you can build anything and it will work after you have collapsed the object, but not even after, i.e. not while it has not been collapsed. Think of caches as of as parts of your scene graph Cinema is completely ignoring regarding the execution of its passes. This little example will demonstrate the effect (see the docstring for details).

    """Put this into a Python Generator Object. 
    
    Playing back the document will not change the output of the Generator, even 
    when 'Optimize Cache' is turned off (i.e. forcing the generator to be 
    evaluated each frame), because Cinema will not evaluate caches.
    
    But when the generator object is converted into its cache state via 'Current 
    State to Object', the Python Tag sitting on the spline object will start to 
    drive the radius of the circle when the document is being played back.
    """ 
    
    import c4d
    
    # Code for a python tag that drives the radius of the circle spline object
    # it is being attached to with the current document time.
    PYTHON_TAG_SCRIPT = """
    import c4d
    
    def main():
        obj = op.GetObject()
        t = (doc.GetTime().Get() % 2.) * 100
        obj[c4d.PRIM_CIRCLE_RADIUS] = t
    """
    
    def main():
        """
        """
        circle_obj = c4d.BaseList2D(c4d.Osplinecircle)
        python_tag = c4d.BaseList2D(c4d.Tpython)
        circle_obj.InsertTag(python_tag)
        python_tag[c4d.TPYTHON_CODE] = PYTHON_TAG_SCRIPT
    
        return circle_obj
    
    

    And just as a friendly reminder, when you do not want to wait for MAXON responding to the AddPort situation - you can always replace Xpresso with a Python tag.

    Cheers,
    zipit



  • Hi @zipit - Thanks again for all your help and guidance with this.

    That last code you sent was really beneficial in helping me get a better understanding of how Python Generators work. I've got my Python Generator code working now just as I intended.

    Also, creating python tags rather than xpresso is a much more elegant way to achieve what I'm trying to do. I'm guessing that Python Generators might not be the best way to introduce yourself to Python in C4D?

    Having used AE for years and utilising Javascript expressions, I'm really excited about what python can open up to me with regards to C4D work. 🙂



  • Hi,

    I'm guessing that Python Generators might not be the best way to introduce yourself to Python in C4D?

    Well, that is a question to which only very subjective answers do exist. But I would say that the Python scripting environment of Cinema (i.e Python Script Manager scripts, Python generator objects, Python tags and the Python Xpresso node) are probably the easiest way to get to know the Cinema 4D SDK.

    There will be some hoops you will have to jump through, some mistakes you will make, no matter if you are starting with C++ plugins, Python plugins or the scripting environment. But the scripting environment is very powerful while considerably cutting down the fluff that usually comes with developing a full blown plugin. A nice side effect is probably also that the scripting environment is arguably the most rapid development environment.

    So I would actually say the Python generator object and the Python tag are just the right place to start.

    Cheers,
    zipit



  • Hi sorry to jump on this topic so late, and I agree that if you could do what you want in a Python Scripting tag its preferred, the mains issue with xpresso is that its a whole system that assumes certain things and was really not designed to be embedded within a generator.

    So the issue about the AddNode, not working is because user data things, as an object xpresso node, is something dynamic (they will change according to the linked object) and I found no way except via the UI (by asking a UI Redraw or a whole scene execution) to refresh these data so the GvPort can be created.

    But since you are in a Generator you don't have access to the GUI since all GUI operation should be done in the Main thread see Threading Information.

    Cheers,
    Maxime.


Log in to reply