Access Spline Field Parameters with XPresso

Hello,

I want to create an xpresso which accesses the offset parameter of a spline field layer in a plain effector. This proves to be more difficult than I thought because, unlike e.g. a linear field, the spline field layer is not a regular object in the hierarchy.

Naively I thought this would work with the general pattern of adding a port to the xpresso node using the description id:

offset_port = gv_node.AddPort(c4d.GV_PORT_INPUT, DESC_ID_OF_PARAMETER )

But I don't know what the description id should be in this case. Or if that is even the right way to go in this situation.

Dragging the parameter into the console returns:

Plain[c4d.FIELDS,11,1011]

Help is appreciated

Hello @interfaceguy,

thank you for reaching out to us. It is a bit unclear to us what you want to achieve here. But when I understand you correctly, you want to:

  1. Create an object node N for an effector object E in an Xpresso graph.
  2. E has a field list setup which contains at least one spline object field F somewhere.
  3. You then want to create an output port on N for F in E.

I.e., you want something like this:
Screenshot 2022-10-07 at 14.00.28.png

There are multiple problems with this, and the major one is that c4d.FieldList parameters provide dynamic descriptions, so while [c4d.FIELDS,11] might work to address a spline object field in one effector, it might not in another one. It works a bit like user data access where [c4d.ID_USERDATA, 1] will also address different things depending on the node.

The second problem is that you must be much more verbose when defining DescID's in some places, as for example adding ports. This topic has been discussed multiple times in the past, the most recent thread can be found here.

I have provided also a solution for your problem below.

Cheers,
Ferdinand

"""Provides an example for adding ports for dynamic description elements.

Run this example with any kind of effector selected which has at least one spline object field in
its field list. The script will then create an xpresso tag and place an object node in it which has
output ports for each offset parameter for each spline object field.
"""

import c4d

from c4d.modules import graphview
from typing import Optional

doc: c4d.documents.BaseDocument  # The active document.
op: Optional[c4d.BaseObject] # The currently selected object, can be #None.

# The maximum amount of field layers which are attempted to be accessed.
MAX_FIELD_LAYER_SEARCH_INDEX: int = 100

def main() -> None:
    """
    """
    # Create an expresso tag on the selected object when it is any kind of effector.
    if not isinstance(op, c4d.BaseObject) or not op.IsInstanceOf(c4d.Obaseeffector):
        raise TypeError("Selected object is not an effector.")

    tag: graphview.XPressoTag = op.MakeTag(c4d.Texpresso)
    if not isinstance(tag, graphview.XPressoTag):
        raise TypeError("Could not allocate xpresso tag.")

    # Get the root node in the tag.
    nodeMaster: graphview.GvNodeMaster = tag.GetNodeMaster()
    root: graphview.GvNode = nodeMaster.GetRoot()
    if not isinstance(root, graphview.GvNode):
        raise RuntimeError("Xpresso tag has not been initialized.")

    # The part where we have to get creative. Field lists are a bit complicated and heavily modify 
    # the description/GUI of the node they are hosted by. So, when you have for example a plain 
    # effector #p, and you want to find the DescID for the #offset parameter of a spline field #f in
    # it,then you cannot simply call p.GetDescription(0) to traverse and find such offset parameter
    # as it is not part of the static description of #p. For the same reason we also cannot just 
    # hardcode such IDs, as they depend on the makeup of the field list. So, while (FIELDS, 11, 1101)
    # might work on one effector to get the offset parameter, it might not work on another one,
    # depending on how it is composed.

    # The route I took here is simply trying out many parameters and checking if the returned data type
    # aligns with what the data type of that parameter should be.

    outPortIds: list[c4d.DescID] = [] # The resulting desc ids.
    fieldLayerIndex: int # The index of a field layer inside a field list.

    # Iterate over all field layers in the field list, the index starts at #10 and we try out 100
    # layer indices after that, i.e., the code below would find #offset parameters in up to one-hundred
    # layers.
    for fieldLayerIndex in range(10, MAX_FIELD_LAYER_SEARCH_INDEX  + 11, 1):

        # Constructing the DescID for the parameter, we must be precise here, as otherwise parameter
        # access and xpresso port creation won't work. This is basically the same as [FIELDS, 11, 1011]
        # for example, but more verbose.
        descId = c4d.DescID(
            # The level for the field list parameter in the effector itself.
            c4d.DescLevel(c4d.FIELDS, c4d.CUSTOMDATATYPE_FIELDLIST, 0),
            # The level for the field layer which is indexed.
            c4d.DescLevel(fieldLayerIndex, c4d.DTYPE_SUBCONTAINER, 0),
            # And finally the parameter which is meant to be indexed. This DescLevel is kept 
            # deliberately less verbose by not providing a data type or creator id.
            c4d.DescLevel(c4d.FIELDLAYER_SPLINE_OFFSET))

        # Get the value for #descId from the effector and append it to the result when the return
        # value is of the expected data type, float in this case for #FIELDLAYER_SPLINE_OFFSET.
        value: any = op.GetParameter(descId, c4d.DESCFLAGS_GET_NONE)
        if isinstance(value, float):
            outPortIds.append(descId)

    # Add a new object node to the xpresso graph, link the effector in it, and then start adding
    # output ports for all DescId in #outPortIds.
    node: graphview.GvNode = nodeMaster.CreateNode(root, c4d.ID_OPERATOR_OBJECT)
    node[c4d.GV_OBJECT_OBJECT_ID] = op

    for descId in outPortIds:
        node.AddPort(c4d.GV_PORT_OUTPUT, descId, message=True)

    # Push an update event to Cinema 4D.
    c4d.EventAdd()

if __name__ == '__main__':
    main()

Thank you for the elaborate reply, it worked! (And helped me to better understand the desc id system)