SOLVED How to add a Userdata child to a group via script

I am a little bit struggling to add userdata into a group.

I couldn´t find something in the forum...

So Example, I just want to add per script a group as userdata and want to put the child into this group.
do I have to use the DESC_PARENTGROUP or DESC_CHILDREN? or whatever??????????

def main():

   op=doc.GetActiveObject()

   bc=c4d.GetCustomDataTypeDefault(c4d.DTYPE_GROUP)
   bc[c4d.DESC_NAME]="Mother"
   mother=op.AddUserData(bc)

   bc=c4d.GetCustomDataTypeDefault(c4d.DTYPE_LONG)
   bc[c4d.DESC_NAME]="Child"
   child=op.AddUserData(bc)
   child=5

Sorry for this beginner question......but you know , the road is rocky

Hello @thomasb,

I am not sure what you mean by locked, but your posting should not be locked from editing.

Regarding DescID, while they are not trivial, they are also not as hard to understand as it might look at the first glance. First of all, we have our C++ DescID Manual, containing information which can be transferred to Python in most parts. There was also the thread PYTHON - Userdata CTracks and Basecontainer in the past, where we did discuss DescID in a bit more detail.

For your specific case, the condensed version would be that DescID are ids for a description. So, they are used address parameters in an object, tag, material, etc. These IDs can be stated in many ways, with varying degrees of verboseness. This especially true for Python which simplifies this even further. See example at the end for details.

Cheers,
Ferdinand

"""Examples for DescID instances.

To be run in the script manger with an object selected.

As discussed in:
    https://plugincafe.maxon.net/topic/13517
"""

import c4d

def main():
    """
    """
    # If the is no active object, then bail.
    if not isinstance(op, c4d.BaseObject):
        raise RuntimeError("Please select an object.")

    # --- Access a description element ---

    # The most simple form of addressing the position of an object. The symbol
    # is just a (whole) number.
    print (c4d.ID_BASEOBJECT_REL_POSITION) # Will print 903

    # We can read a parameter a node in Python with bracket syntax. By either 
    # passing in the number or the symbol.
    print (op[903])
    print (op[c4d.ID_BASEOBJECT_REL_POSITION])

    # We could also use other access interfaces like C4DAtom.Get/SetParameter
    # or BaseContainer access, but I am going to ignore them here.

    # However, we can also wrap this number in a DescID in the following way.
    descId = c4d.DescID(c4d.ID_BASEOBJECT_REL_POSITION)
    # And pass that in instead. It will do exactly the same as the previous 
    # call op[c4d.ID_BASEOBJECT_REL_POSITION]
    print (op[descId])

    # DescIDs are formally constructed from DescLevels, what we did above
    # was just a shorthand for this. A DescLevel just wraps around a number.
    # So, we can state c4d.DescID(c4d.ID_BASEOBJECT_REL_POSITION) also like
    # this.
    descLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION)
    descId = c4d.DescID(descLevel)
    print (op[descId])

    # However, DescLevels can convey more information than just the ID,
    # they can also store the data type and the creator of that id. The 
    # position is a vector, so we can state that. The 0 at the end sort
    # of means no creator or "whatever" ^^
    descLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, 
                              c4d.DTYPE_VECTOR, 0)
    descId = c4d.DescID(descLevel)
    print (op[descId])

    # So, to recap, these five ids are more or less all equivalent, they at
    # least will return all the same parameter value.
    #
    #   903
    #   c4d.ID_BASEOBJECT_REL_POSITION
    #   c4d.DescID(c4d.ID_BASEOBJECT_REL_POSITION)
    #   c4d.DescID(c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION)) 
    #   c4d.DescID(c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, 
    #                            c4d.DTYPE_VECTOR, 0))

    # --- Access the component of a description element ---

    # Until now, we have only accessed the vector as a whole. But there are
    # some data types which are composed out of sub-components, e.g., 
    # BaseContainer or Vector. Which is why a DescID can be composed out of
    # three IDs, where each level (DescLevel) further specifies which part
    # of the parameter we want to access.

    print (c4d.VECTOR_X) # Will print 1000

    # To access the X component of the position vector, we can do this.
    print (op[903, 1000])
    print (op[c4d.ID_BASEOBJECT_REL_POSITION, c4d.VECTOR_X])

    # Just as before, this can be done more verbosely with DescID to varying 
    # degrees.

    # a.
    dLvlVector = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION)
    dLvlVectorX = c4d.DescLevel(c4d.VECTOR_X)
    descId = c4d.DescID(dLvlVector, dLvlVectorX)
    print (op[descId])

    # b.
    dLvlVector = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, 
                               c4d.DTYPE_VECTOR, 0)
    dLvlVectorX = c4d.DescLevel(c4d.VECTOR_X, 
                                c4d.DTYPE_REAL, 0)
    descId = c4d.DescID(dLvlVector, dLvlVectorX)
    print (op[descId])

    # So, this part in the other code I did post:
    #
    #   descId = c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA, 5, 0),
    #                       c4d.DescLevel(1, 1, 0))
    #
    # was written a bit lazily, a more verbose form would be:
    #
    #   descId = c4d.DescID(
    #       c4d.DescLevel(c4d.ID_USERDATA, c4d.DTYPE_SUBCONTAINER, 0),
    #       c4d.DescLevel(1, c4d.DTYPE_GROUP, 0))
    #
    # It is just a complicated way of saying (700, 1) or in symbolic form 
    # (c4d.ID_USERDATA, 1). It just means the first element in the user data
    # container. However, in some places, e.g., descriptions or xpresso nodes,
    # this very verbose form is required by Cinema, as all other are just a
    # shorthand form for user convenience.

if __name__ == '__main__':
    main()

Hello @ThomasB,

thank you for reaching out to us. Please remember to add the required information to your posting as stated in the Forum Guidelines: Tagging. It is getting a bit out of hand in the last time that people skip that information. If we do not need it, we sometimes do not bring up the issue, but it is always formally required.

This is not trivial since it requires you to understand some stuff about DescID. I whipped up a small example. If you need further help, please feel free to ask.

Cheers,
Ferdinand

"""Example for programmatically adding user data group elements and populate 
them with stuff.

As discussed in:
    https://plugincafe.maxon.net/topic/13517/
"""

import c4d

def main():
   """
   """
   # Not required, as op is already predefined as the active object in a
   # script manger script.
   # op=doc.GetActiveObject()

   # Create a group element.
   outerGroupData = c4d.GetCustomDataTypeDefault(c4d.DTYPE_GROUP)
   outerGroupData[c4d.DESC_NAME] = "Outer Group"

   # A float element in the outer group.
   outerFloatData = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
   outerFloatData[c4d.DESC_NAME] = "Some Float"
   outerFloatData[c4d.DESC_MIN] = -10
   outerFloatData[c4d.DESC_MAX] = 10
   outerFloatData[c4d.DESC_STEP] = 0.5
   # Attach the float element to the outer group, the first element.
   descId = c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA, 5, 0),
                       c4d.DescLevel(1, 1, 0))
   outerFloatData[c4d.DESC_PARENTGROUP] = descId

   # Create a group element.
   innerGroupData = c4d.GetCustomDataTypeDefault(c4d.DTYPE_GROUP)
   innerGroupData[c4d.DESC_NAME] = "Inner Group"
   # Add the inner group to the outer group, the first element.
   descId = c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA, 5, 0),
                       c4d.DescLevel(1, 1, 0))
   innerGroupData[c4d.DESC_PARENTGROUP] = descId

   # A float element in the inner group.
   innerFloatData = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
   innerFloatData[c4d.DESC_NAME] = "Some Float"
   innerFloatData[c4d.DESC_MIN] = -10
   innerFloatData[c4d.DESC_MAX] = 10
   innerFloatData[c4d.DESC_STEP] = 0.5
   # Attach the float element to the inner group, the third element.
   descId = c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA, 5, 0),
                       c4d.DescLevel(3, 1, 0))
   innerFloatData[c4d.DESC_PARENTGROUP] = descId

   # Add all element, the order matters here, since we index/reference the
   # elements in the DESC_PARENTGROUP values by index, so we have reflect
   # that order here.
   outerGroupId = op.AddUserData(outerGroupData)
   outerFloatId = op.AddUserData(outerFloatData)
   innerGroupId = op.AddUserData(innerGroupData)
   innerFloatId = op.AddUserData(innerFloatData)

   # Set the float values.
   op[outerFloatId] = 3.1415
   op[innerFloatId] = 2. * 3.1415

   # Update Cinema 4D
   c4d.EventAdd()

if __name__ == "__main__":
    main()

First, thanks for your effort, I really appreciate that....

Sorry for this missing tagging thing.....it is easy to overlook that sorry......I will edit it later when it's no more locked to edit.

Yes this DescID thing is so complicated :-), I really should go more in depth here.....

of course I need further help but this would crash the forum 🙂 and the patience of many others

So thank you first Ferdinand

Hello @thomasb,

I am not sure what you mean by locked, but your posting should not be locked from editing.

Regarding DescID, while they are not trivial, they are also not as hard to understand as it might look at the first glance. First of all, we have our C++ DescID Manual, containing information which can be transferred to Python in most parts. There was also the thread PYTHON - Userdata CTracks and Basecontainer in the past, where we did discuss DescID in a bit more detail.

For your specific case, the condensed version would be that DescID are ids for a description. So, they are used address parameters in an object, tag, material, etc. These IDs can be stated in many ways, with varying degrees of verboseness. This especially true for Python which simplifies this even further. See example at the end for details.

Cheers,
Ferdinand

"""Examples for DescID instances.

To be run in the script manger with an object selected.

As discussed in:
    https://plugincafe.maxon.net/topic/13517
"""

import c4d

def main():
    """
    """
    # If the is no active object, then bail.
    if not isinstance(op, c4d.BaseObject):
        raise RuntimeError("Please select an object.")

    # --- Access a description element ---

    # The most simple form of addressing the position of an object. The symbol
    # is just a (whole) number.
    print (c4d.ID_BASEOBJECT_REL_POSITION) # Will print 903

    # We can read a parameter a node in Python with bracket syntax. By either 
    # passing in the number or the symbol.
    print (op[903])
    print (op[c4d.ID_BASEOBJECT_REL_POSITION])

    # We could also use other access interfaces like C4DAtom.Get/SetParameter
    # or BaseContainer access, but I am going to ignore them here.

    # However, we can also wrap this number in a DescID in the following way.
    descId = c4d.DescID(c4d.ID_BASEOBJECT_REL_POSITION)
    # And pass that in instead. It will do exactly the same as the previous 
    # call op[c4d.ID_BASEOBJECT_REL_POSITION]
    print (op[descId])

    # DescIDs are formally constructed from DescLevels, what we did above
    # was just a shorthand for this. A DescLevel just wraps around a number.
    # So, we can state c4d.DescID(c4d.ID_BASEOBJECT_REL_POSITION) also like
    # this.
    descLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION)
    descId = c4d.DescID(descLevel)
    print (op[descId])

    # However, DescLevels can convey more information than just the ID,
    # they can also store the data type and the creator of that id. The 
    # position is a vector, so we can state that. The 0 at the end sort
    # of means no creator or "whatever" ^^
    descLevel = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, 
                              c4d.DTYPE_VECTOR, 0)
    descId = c4d.DescID(descLevel)
    print (op[descId])

    # So, to recap, these five ids are more or less all equivalent, they at
    # least will return all the same parameter value.
    #
    #   903
    #   c4d.ID_BASEOBJECT_REL_POSITION
    #   c4d.DescID(c4d.ID_BASEOBJECT_REL_POSITION)
    #   c4d.DescID(c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION)) 
    #   c4d.DescID(c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, 
    #                            c4d.DTYPE_VECTOR, 0))

    # --- Access the component of a description element ---

    # Until now, we have only accessed the vector as a whole. But there are
    # some data types which are composed out of sub-components, e.g., 
    # BaseContainer or Vector. Which is why a DescID can be composed out of
    # three IDs, where each level (DescLevel) further specifies which part
    # of the parameter we want to access.

    print (c4d.VECTOR_X) # Will print 1000

    # To access the X component of the position vector, we can do this.
    print (op[903, 1000])
    print (op[c4d.ID_BASEOBJECT_REL_POSITION, c4d.VECTOR_X])

    # Just as before, this can be done more verbosely with DescID to varying 
    # degrees.

    # a.
    dLvlVector = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION)
    dLvlVectorX = c4d.DescLevel(c4d.VECTOR_X)
    descId = c4d.DescID(dLvlVector, dLvlVectorX)
    print (op[descId])

    # b.
    dLvlVector = c4d.DescLevel(c4d.ID_BASEOBJECT_REL_POSITION, 
                               c4d.DTYPE_VECTOR, 0)
    dLvlVectorX = c4d.DescLevel(c4d.VECTOR_X, 
                                c4d.DTYPE_REAL, 0)
    descId = c4d.DescID(dLvlVector, dLvlVectorX)
    print (op[descId])

    # So, this part in the other code I did post:
    #
    #   descId = c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA, 5, 0),
    #                       c4d.DescLevel(1, 1, 0))
    #
    # was written a bit lazily, a more verbose form would be:
    #
    #   descId = c4d.DescID(
    #       c4d.DescLevel(c4d.ID_USERDATA, c4d.DTYPE_SUBCONTAINER, 0),
    #       c4d.DescLevel(1, c4d.DTYPE_GROUP, 0))
    #
    # It is just a complicated way of saying (700, 1) or in symbolic form 
    # (c4d.ID_USERDATA, 1). It just means the first element in the user data
    # container. However, in some places, e.g., descriptions or xpresso nodes,
    # this very verbose form is required by Cinema, as all other are just a
    # shorthand form for user convenience.

if __name__ == '__main__':
    main()

Thanks Ferdinand, this helps me lot, I somehow already knew that it was structured that way, but it's not that easy for me :-), because per print you always get this int ID´s , which confused me....

yes, I have already found out that it is quite useful to consult the C ++ API, as it is much more detailed and a lot can of course be taken over ...

Thank you very much for this detailed explanation, that's exactly what I was looking for.
I'll have a look at it in C ++ with the description etc.

  • But what does that last 0 id mean in the DescLevel??? Is this maybe for CustomGui and cycle for example , when there is a another Level.
    And is it possible maybe to set the DESC_MAX value dynamically with a Python Generator?**
    so that I force maybe the DESC_MAX value to be True when a certain value is reached?

Heelo @ThomasB ,

@thomasb said in How to add a Userdata child to a group via script:

But what does that last 0 id mean in the DescLevel??? Is this maybe for CustomGui and cycle for example , when there is a another Level.
And is it possible maybe to set the DESC_MAX value dynamically with a Python Generator?**
so that I force maybe the DESC_MAX value to be True when a certain value is reached?

I'll just quote myself here from the commented code above:

However, DescLevel can convey more information than just the ID, they can also store the data type and the creator of that id. The position is a vector, so we can state that. The 0 at the end sort of means no creator or "whatever" ^^

So, a DescLevel stores the tuple (id, dataType, creatorId). A DescID can store up to three DescLevel, which means you could for example address the x-component of a vector that is the first user-data element like this: node[c4d.ID_USERDATA, 1, c4d.VECTOR_X]. Cycle elements have nothing to do with this and are only relevant within a description. Drop-down boxes are always just integers in Cinema, or specifically a form of GUI for a LONG description element, you have no means of interacting with the drop-down from the outside world.

You can sort of realize dynamic GUIs with user data from a scripting objects side by constantly rebuilding the user data container. But that is not really how these things are intended to be used. You would be better of then just writing a plugin and overwrite NodeData.GetDDescription which is meant to dynamically modify the description of a node. It seems not advisable though to dynamically modify DESC_MAX as this might confuse the user. If you just want to clamp a value based on other settings, I would just do this in NodeData.Message. This year we had a live-talk where I presented a little script object example which sort of does this. It clamps two integer sliders based on the value of the other. You can find the example at Plugin Café GitHub: Praxiswochen. You will find the related code in the message function.

Cheers,
Ferdinand

@ferdinand

you are so crazy Ferdinand, you helped me really .....you should make a paid workshop about that 😊

I prefer to use this notation.......it is ok or?

motherDescId=c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA,c4d.DTYPE_SUBCONTAINER,0),c4d.DescLevel(1,c4d.DTYPE_GROUP, 0))

and also your code example with getting the ID , very detailed and useful, thank's for that @ferdinand .

This c++ SDK documentation is amazing and much better than the python one....
The search function is awesome

Hello @thomasb,

@thomasb said:

I prefer to use this notation.......it is ok or?

motherDescId=c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA,c4d.DTYPE_SUBCONTAINER,0),c4d.DescLevel(1,c4d.DTYPE_GROUP, 0))

Yes, that is okay. In fact, this most verbose form is the only form which is guaranteed to always to be sufficient when addressing a description element. But as stated earlier, the instances where that verbose form is required are quite rare; modifying a description like discussed here and addressing c4d.modules.graphview ports are in fact the only instances I can think of out of my head in the Python SDK.

I personally would always gravitate towards the shorthand form, i.e., myNode[c4d.ID_USERDATA, 1] in this case, wherever possible, as the verbose form is a bit confusing to read.

Cheers,
Ferdinand