SOLVED Get Description IDs for all ports (exposed, unexposed) of a GvNode

Hello dear people,

After hours of trying, hoping, searching and crying I post again.

I want to read out the complete description of a GvNode. Reading out parameters and sub-channels I got to work like this.

desc = node.GetDescription(c4d.DESCFLAGS_DESC_NONE) #c4d.Description
    for bc, paramid, groupid in  desc:
        #the ID of a parameter
        name = bc[c4d.DESC_IDENT]

        subd = c4d.Description()
            if subd.LoadDescription(paramid[0].dtype):
                for bcB, paramidB, groupidB in  subd:
                    #the ID of a sub-channel
                    subname = bcB[c4d.DESC_IDENT]

through this I also can get some ports. For example I can get GV_POINT_INPUT_OBJECT and GV_POINT_INPUT_POINT. (maybe because they are static? but on the other hand GV_POINT_OUTPUT_POINT is as well).
But other ports I do not get. It seems to me I should get them, since they are in the description res file.

32a8125c-ef99-4ba2-8e6f-7c0f1073d94a-grafik.png

Does anybody have an idea how to get all the IDs for all avalible ports (exposed and unexposed)? It needs to be programmatically too.

Cheers,
David

Hello @davidweidemann,

Thank you for reaching out to us. Please make sure to provide executable code when you ask questions, as otherwise people answering will have to write boiler plate code to get up to the point where the question starts. In this case: how you get hold of node. I had some code flying around I could use in this case, but this is not always the case. This is also the reason why my code is so long, as I simply reused an older script of mine.

The fundamental assumption seems to be here that a description resource (a .res file) and a description are the same. But they are not as you already indicate yourself with the terms "exposed and unexposed". The static description, what is returned by c4d.C4DAtom.GetDescription() and the dynamic description, what is roughly represented by c4d.BaseList2D.GetUserDataContainer() in Python, are the data for the parameters a node holds. There is no explicit guarantee that the static description is the same as what is the content of its source resource file. Xpresso is one of the examples where the classic API shuffles things around in the background. So, when you have a point node which has only the four default ports exposed in a graph, e.g., this:

b4be0f1b-ce3d-4bf6-aca8-4d20b67b5f05-image.png

Then there is nothing more to find in the description of that node. If you run the script posted below on a scene containing such node, it will spit out:

The Xpresso tag XPresso at 2133138259008 has the nodes:
	<c4d.modules.graphview.GvNode object called XGroup/XGroup with ID 1001102 at 2133138144512>
	<c4d.modules.graphview.GvNode object called Point/Point with ID 1001101 at 2133138064000>
		Object: <c4d.modules.graphview.GvPort object at 0x000001F0A8F117C0>
		Point Index: <c4d.modules.graphview.GvPort object at 0x000001F0A8F23440>
		Point Count: <c4d.modules.graphview.GvPort object at 0x000001F0A8F0F8C0>
		Point Position: <c4d.modules.graphview.GvPort object at 0x000001F0A8EF5880>

However, when you add some of the remaining ports, it will spit out the following, which then will also be reflected in the description of the node:

6885b993-c07b-44bc-8412-1f7e866fd4b5-image.png

The Xpresso tag XPresso at 2133138137216 has the nodes:
	<c4d.modules.graphview.GvNode object called XGroup/XGroup with ID 1001102 at 2133138141248>
	<c4d.modules.graphview.GvNode object called Point/Point with ID 1001101 at 2133138248832>
		Object: <c4d.modules.graphview.GvPort object at 0x000001F0A8EF3900>
		Point Index: <c4d.modules.graphview.GvPort object at 0x000001F0A8F29AC0>
		Point Position: <c4d.modules.graphview.GvPort object at 0x000001F0A8F24840>
		Point Count: <c4d.modules.graphview.GvPort object at 0x000001F0A8EF1600>
		Point Position: <c4d.modules.graphview.GvPort object at 0x000001F0A8EF5F80>
		Point Normal: <c4d.modules.graphview.GvPort object at 0x000001F0A8F1C240>

When you want access to the potential ports of a node, you will have to parse its resource file manually. Which is possible in theory, due to resource file names being derivable from type symbols in Cinema 4D, but it is nothing our APIs provide any interfaces for.

Cheers,
Ferdinand

The code I used to generate the quoted outputs:

"""Example for iterating over all Xpresso nodes in a scene.

Node iteration can be quite a complex topic in Cinema 4D due to the many graph
relations that are contained in a document and the problems that arise from 
recursive solutions for the problem - stack overflows or in Python the safety 
measures of Python preventing them. We have this topic on our bucket list in 
the SDK-Team, but for now one has to approach it with self-provided solutions.

This is an example pattern for a node iterator (which does not take caches 
into account). One can throw it at any kind of GeListNode and it will yield in
a stack-safe manner all next-siblings and descendants of a node. How this
iteration is carried out (depth or breadth first), to include any kind of
siblings, not just next-siblings, to include also ancestors and many things
more could be done differently. This pattern is tailored relatively closely
to what the topic demanded. A more versatile solution would have to be
provided in the SDK.
"""
import c4d

# This is a node iteration implementation. To a certain degree this can be
# treated as a black box and does not have to be understood. This will
# however change when one wants to include other use-case scenarios for which
# one would have to modify it.
def NodeIterator(node, types=None):
    """An iterator for a GeListNode node graph with optional filtering.

    Will yield all  downward siblings and all descendants of a node. Will
    not yield any ancestors of the node. Is stack overflow (prevention) safe
    due to being truly iterative. Alsob provides a filter mechanism which 
    accepts an ineteger type symbol filter for the yielded nodes.

    Args:
        node (c4d.GeListNode): The starting node for which to yield all next-
         siblings and descendants.
        types (None | int | tuple[int]), optional): The optional type 
         filter. If None, all nodes will bey yielded. If not None, only nodes 
         that inherit from at least one of the type symbols defined in the 
         filter tuple will be yielded. Defaults to None. 

    Yields:
        c4d.GeListNode: A node in the iteration sequence.

    Raises:
        TypeError: On argument type oopses.
    """
    # Some argument massaging and validation.
    if not isinstance(node, c4d.GeListNode):
        msg = "Expected a GeListNode or derived class, got: {0}"
        raise TypeError(msg.format(node.__class__.__name__))

    if isinstance(types, int):
        types = (types, )
    if not isinstance(types, (tuple, list, type(None))):
        msg = "Expected a tuple, list or None, got: {0}"
        raise TypeError(msg.format(types.__class__.__name__))

    def iterate(node, types=None):
        """The iteration function, walking a graph depth first.

        Args:
            same as outer function.

        Yields:
            same as outer function.
        """
        # This or specifically the usage of the lookup later is not ideal
        # performance-wise. But without making this super-complicated, this
        # is the best solution I see here (as we otherwise would end up with
        # three versions of what the code does below).
        visisted = []
        terminal = node.GetUp() if isinstance(node, c4d.GeListNode) else None

        while node:
            if node not in visisted:
                if types is None or any([node.IsInstanceOf(t) for t in types]):
                    yield node
                visisted.append(node)

            if node.GetDown() and node.GetDown() not in visisted:
                node = node.GetDown()
            elif node.GetNext():
                node = node.GetNext()
            else:
                node = node.GetUp()

            if node == terminal:
                break

    # For each next-sibling or descendant of the node passed to this call:
    for iterant in iterate(node):
        # Yield it if it does match our type filter.
        if types is None or any([iterant.IsInstanceOf(t) for t in types]):
            yield iterant

        # And iterate its tags if it is BaseObject.
        if isinstance(iterant, c4d.BaseObject):
            for tag in iterate(iterant.GetFirstTag(), types):
                yield tag


def main():
    """Entry point.
    """
    # We iterate over all Xpresso tags in the document. We pass in the first 
    # object in the scene and specify that we are only interested in nodes of 
    # type c4d.Texpresso (Xpresso tags).
    for xpressoTag in NodeIterator(doc.GetFirstObject(), c4d.Texpresso):
        # Print out some stuff about the currently yielded Xpresso tag.
        name, nid = xpressoTag.GetName(), id(xpressoTag)
        print(f"The Xpresso tag {name} at {nid} has the nodes:")
        # Get its master node and root node in the Xpresso graph.
        masterNode = xpressoTag.GetNodeMaster()
        root = masterNode.GetRoot()
        # And iterate over all nodes in that root node.
        for xpressoNode in NodeIterator(root):
            # Print out a node.
            print(f"\t{xpressoNode}")

            # Print out its ports.
            portCollection = xpressoNode.GetInPorts() + xpressoNode.GetOutPorts()
            portCount = len(portCollection)

            if portCount < 1:
                continue

            for port in portCollection:
                print (f"\t\t{port.GetName(xpressoNode)}: {port}")


if __name__ == '__main__':
    main()

Thank you ferdidand,

Sorry for not giving more contextual code...

Yeah "potential ports" is what I would be interested in. Thanks for confirming that there is nothing in the API that would help me though!