SOLVED How to Use ListAllNodes?

Hi,

I'm a bit lost on how to use the ListAllNodes, specifically the matchingData parameter. It accepts a DataDictionary.

But there is no illustration examples of the DataDictionary on the Python Documentation (or on forum, at least relating to node set-up).

Here is what I have so far. It does not error out. So I'm not sure what's not working.

import maxon
import c4d
import maxon.frameworks.nodespace
import maxon.frameworks.nodes

def main():
    mat = doc.GetActiveMaterial()
    nodeMaterial = mat.GetNodeMaterialReference()
    nodespaceId = c4d.GetActiveNodeSpaceId()
    nimbusRef = mat.GetNimbusRef(nodespaceId)
    graph = nimbusRef.GetGraph()

    def pseudo_function(node):
        # Pseudo function since ListAllNodes requires a callback function
        print (node)
        return

    with graph.BeginTransaction() as transaction:
        matching_data = maxon.DataDictionary()

        # The hardcoded string values were retrieved from copy pasting a node to sublime text
        matching_data.Set("baseId", "com.redshift3d.redshift4c4d.nodes.core.texturesampler")
        graph.ListAllNodes(pseudo_function, matching_data)
        transaction.Commit()

    c4d.EventAdd()

if __name__ == "__main__":
    main()

Hello @bentraje,

Thank you for reaching out to us. You were using ListAllNodes incorrectly, but that is of little consequence, as there are multiple other problems in the Python Nodes API around the subject of what you want to do.

To get first the little mistake out of the way: The maxon API is inherently based on the type maxon.Id to identify things. So, to search for example for nodes with a specific comment, you would have to do this.

data.Set(maxon.NODE.BASE.COMMENT, "Blah, blah, blah, ...")
graph.ListAllNodes(myCallBack, data)

where maxon.NODE.BASE.COMMENT is a predefined ID for the comment attribute of a node. But:

  1. There is for some reason no attribute for the ID of a node, although our C++ docs claim that you can search for nodes by ID with ListAllNodes.
  2. The method is broken in Python (25 and 2023.1) and maybe even in C++, I could only get it to work with the COMMENT attribute, and not for example with the maxon.NODE.BASE.NAME or maxon.OBJECT.BASE.NAME attribute.

I then tried going the NodePath route I mentioned in the other thread, and while they technically work, this approach will be stopped in Python by being unable to construct the node paths manually with NodePathInterface.FromCString due to the fact that we cannot construct a maxon::Block<maxon::Char> in Python (again, in any version).

So, the only route for you to take here is to either:

  1. Follow the patterns recommended in our example scripts (I would still recommend this).
  2. Or simply write your own little node traversal function. But as hinted at in the other thread, throwing the fictitious node pathroot>someNode>somePortBundle>somePort at some graph is ambigous, as there can be multiple graph elements in a graph which match this path.

Find a little write up for the second case at the end of my posting.

Cheers,
Ferdinand

The scene: scene.c4d
The output for the example scene:

root = Root
material = material
bsdfLayer = material>materialout/layers/1/bsdflayer
alsoBsdfLayer = material>materialout/layers/1/bsdflayer
alsoalsoBsdfLayer = material>materialout/layers/1/bsdflayer
alsoalsoBsdfLayer.GetPath() = material>materialout/layers/1/bsdflayer


The node graph for [email protected]:
''(kind = 1 at 0x00000248124120D0)
 '>'(kind = 4 at 0x000002481243EA90)
 '<'(kind = 2 at 0x000002481243F850)
 'material'(kind = 1 at 0x000002481240B810)
  '>'(kind = 4 at 0x0000024812435690)
   'materialout'(kind = 16 at 0x000002481242E390)
    'aovs'(kind = 16 at 0x000002481243FD10)
    'spdlevel'(kind = 16 at 0x0000024812436050)
    'roundgeometry'(kind = 16 at 0x0000024812417310)
    'spd'(kind = 16 at 0x0000024812429DD0)
    'displacementheight'(kind = 16 at 0x000002481241F750)
    'displacement'(kind = 16 at 0x0000024812436190)
    'absorption'(kind = 16 at 0x0000024812430390)
    'blurriness'(kind = 16 at 0x000002481243BC90)
    'samplesubdiv'(kind = 16 at 0x0000024812436ED0)
    'normal'(kind = 16 at 0x0000024812408F50)
    'absorptiondistance'(kind = 16 at 0x0000024812435C50)
    'fresnelreflectivity'(kind = 16 at 0x000002481241AB50)
    'roughness'(kind = 16 at 0x0000024812425AD0)
    'ior'(kind = 16 at 0x0000024812412C10)
    'exitreflections'(kind = 16 at 0x0000024812419F10)
    'transparency'(kind = 16 at 0x000002481242CED0)
    'enabletransparency'(kind = 16 at 0x0000024812407610)
    'alphacontrol'(kind = 16 at 0x000002481240DF90)
    'alpha'(kind = 16 at 0x000002481243D790)
    'emission'(kind = 16 at 0x000002481240DE10)
    'layers'(kind = 16 at 0x0000024812428210)
     '1'(kind = 16 at 0x0000024812411E50)
      'active'(kind = 16 at 0x00000248124337D0)
      'bsdflayer'(kind = 16 at 0x0000024812402890)
       'color'(kind = 16 at 0x0000024812408AD0)
       'samplesubdiv'(kind = 16 at 0x0000024812419450)
       'normal'(kind = 16 at 0x0000024812441BD0)
       'roughness'(kind = 16 at 0x000002481243FA90)
       'opaquefresnel'(kind = 16 at 0x000002481243B510)
       'dielectricior'(kind = 16 at 0x0000024812431D50)
       'conductorior'(kind = 16 at 0x0000024812425950)
       'fresnelstrength'(kind = 16 at 0x0000024812431250)
       'conductorabs'(kind = 16 at 0x0000024812430D90)
       'normalstrength'(kind = 16 at 0x000002481243E790)
       'fresneltype'(kind = 16 at 0x000002481240B390)
       'orientation'(kind = 16 at 0x000002481242E490)
       'anisotropy'(kind = 16 at 0x000002481242E090)
       'reflectionstrength'(kind = 16 at 0x0000024812434290)
       'bsdftype'(kind = 16 at 0x0000024812438F50)
      'strength'(kind = 16 at 0x000002481242B710)
  '<'(kind = 2 at 0x000002481241E190)
   'context'(kind = 8 at 0x0000024812431350)
   'aovs'(kind = 8 at 0x0000024812424090)
   'spdlevel'(kind = 8 at 0x000002481243FAD0)
   'roundgeometry'(kind = 8 at 0x000002481242C190)
   'spd'(kind = 8 at 0x0000024812415910)
   'displacementheight'(kind = 8 at 0x000002481241C310)
   'displacement'(kind = 8 at 0x000002481240BED0)
   'absorption'(kind = 8 at 0x0000024812425810)
   'blurriness'(kind = 8 at 0x000002481243B750)
   'samplesubdiv'(kind = 8 at 0x000002481243AB90)
   'normal'(kind = 8 at 0x0000024812410110)
   'absorptiondistance'(kind = 8 at 0x000002481242B8D0)
   'fresnelreflectivity'(kind = 8 at 0x000002481240E110)
   'roughness'(kind = 8 at 0x0000024812413BD0)
   'ior'(kind = 8 at 0x0000024812425690)
   'exitreflections'(kind = 8 at 0x0000024812407B10)
   'transparency'(kind = 8 at 0x000002481242ED50)
   'enabletransparency'(kind = 8 at 0x0000024812440590)
   'alphacontrol'(kind = 8 at 0x000002481241B550)
   'alpha'(kind = 8 at 0x000002481241FF10)
   'emission'(kind = 8 at 0x000002481242AB50)
   'bsdflayers'(kind = 8 at 0x0000024812423BD0)
    '1'(kind = 8 at 0x0000024812407110)
     'active'(kind = 8 at 0x000002481243C6D0)
     'bsdflayer'(kind = 8 at 0x0000024812415F10)
      'color'(kind = 8 at 0x0000024812418010)
      'samplesubdiv'(kind = 8 at 0x0000024812415950)
      'normal'(kind = 8 at 0x000002481242EA50)
      'roughness'(kind = 8 at 0x000002481242D510)
      'opaquefresnel'(kind = 8 at 0x0000024812409C10)
      'dielectricior'(kind = 8 at 0x0000024812418890)
      'conductorior'(kind = 8 at 0x0000024812408690)
      'fresnelstrength'(kind = 8 at 0x000002481242DE90)
      'conductorabs'(kind = 8 at 0x00000248124270D0)
      'normalstrength'(kind = 8 at 0x00000248124387D0)
      'fresneltype'(kind = 8 at 0x00000248124283D0)
      'orientation'(kind = 8 at 0x0000024812418450)
      'anisotropy'(kind = 8 at 0x00000248124396D0)
      'reflectionstrength'(kind = 8 at 0x0000024812413810)
      'bsdftype'(kind = 8 at 0x0000024812434F90)
     'strength'(kind = 8 at 0x000002481241C790)
   'globalcontext'(kind = 8 at 0x0000024812436F10)
 'bsdf'(kind = 1 at 0x000002481242E450)
  '>'(kind = 4 at 0x0000024812437850)
   'result'(kind = 16 at 0x0000024812403710)
    'color'(kind = 16 at 0x000002481242E110)
    'samplesubdiv'(kind = 16 at 0x0000024812413C50)
    'normal'(kind = 16 at 0x0000024812441B90)
    'roughness'(kind = 16 at 0x00000248124054D0)
    'opaquefresnel'(kind = 16 at 0x000002481242C710)
    'dielectricior'(kind = 16 at 0x0000024812423010)
    'conductorior'(kind = 16 at 0x000002481240A4D0)
    'fresnelstrength'(kind = 16 at 0x000002481242BF90)
    'conductorabs'(kind = 16 at 0x0000024812402C90)
    'normalstrength'(kind = 16 at 0x0000024812437510)
    'fresneltype'(kind = 16 at 0x0000024812429D90)
    'orientation'(kind = 16 at 0x0000024812413610)
    'anisotropy'(kind = 16 at 0x0000024812422F50)
    'reflectionstrength'(kind = 16 at 0x0000024812410C10)
    'bsdftype'(kind = 16 at 0x0000024812430590)
  '<'(kind = 2 at 0x0000024812429E50)
   'color'(kind = 8 at 0x00000248124235D0)
   'context'(kind = 8 at 0x0000024812436E90)
   'samplesubdiv'(kind = 8 at 0x0000024812424110)
   'normal'(kind = 8 at 0x0000024812422990)
   'roughness'(kind = 8 at 0x000002481240F1D0)
   'opaquefresnel'(kind = 8 at 0x0000024812417150)
   'dielectricior'(kind = 8 at 0x0000024812428B50)
   'conductorior'(kind = 8 at 0x000002481243E490)
   'fresnelstrength'(kind = 8 at 0x0000024812424150)
   'conductorabs'(kind = 8 at 0x000002481240D2D0)
   'normalstrength'(kind = 8 at 0x0000024812426550)
   'fresneltype'(kind = 8 at 0x0000024812414590)
   'orientation'(kind = 8 at 0x000002481243C410)
   'anisotropy'(kind = 8 at 0x0000024812415C10)
   'reflectionstrength'(kind = 8 at 0x0000024812433B90)
   'bsdftype'(kind = 8 at 0x00000248124264D0)
>>> 

The code:

"""Demonstrates principal graph traversal in a Nodes API graph to find graph elements.

I changed the example from Redshift to standard materials, so you should use the provided example
scene.
"""

import c4d
import maxon
import typing

import maxon.frameworks.nodes as nodesAPI
import maxon.frameworks.graph as graphAPI

doc: c4d.documents.BaseDocument # The active document.

def FindNode(node: nodesAPI.GraphNode, 
             nid: maxon.Id, 
             kind: typing.Optional[maxon.Id] = None) -> typing.Optional[nodesAPI.GraphNode]:
    """Searches for a node with the given node ID #nid below and including #node.

    The Python Nodes API in R25 an even in 2023.1 is quite buggy and incomplete. GraphModelHelper/
    GraphModelInterface.ListAllNode does not work properly in either versions. NodePaths do work,
    you are however unable to construct them, as you are unable to construct a 
    maxon::Block<maxon::Char>, even when taking the scenic route over maxon::BaseArray. The most 
    bare-bones solution is then to simply write the necessary functions yourself (at least when 
    possible).

    The type GraphNode is named a bit confusingly, a better name would be GraphElement. The "node"
    part does not refer to nodes in the sense of a node as seen in the GUI, but the hierarchical 
    data structure within which elements of a graph are placed. Which are of course also nodes, but
    in a different sense. 
    
    When we take a generic graph, the internal tree structure of that graph is in principal as shown
    below. I will use the word "node" exclusively for what end users call node, and use in other 
    cases the word element or port. Please also note that what I show here is a simplification, see
    and use PrintTree() for how node elements are practically structured.

        graph (NodesGraphModelRef): Not a node and just for completeness.

        "root" (GraphNode): The root element of the graph, returned by graph.GetRoot().
            + "Material3" (GraphNode): The first tangible node in the graph data structure, there is
            |   |                      no higher meaning behind its position in this tree, this 
            |   |                      could also be the last node the user added in the GUI, or
            |   |                      the node which is "visually" last.
            |   + ">" (GraphNode): A graph element that contains all ingoing ports of Material 3,
            |   |   |              this is equivalent to Material3.GetInPorts().
            |   |   + "Emission" (GraphNode): The emission input port of the node, this is a simple
            |   |   |                         "flat" port.
            |   |   + "Surface" (GraphNode): This is the surface input port of the node, it is a
            |   |       |                    "port bundle", i.e., a port that has child ports which
            |   |       |                    can be set individually.
            |   |       + "Diffuse" (GraphNode): A "Diffuse" child port of the "Surface" port 
            |   |           |                    bundle. It is itself a port bundle.
            |   |           + "BDSF Layer" (GraphNode): A "BDSF Layer" child port of the "Diffuse"
            |   |                                       port bundle.
            |   + "<" (GraphNode): A graph element that contains all outgoing ports of Material 3,
            |       |              this is equivalent to Material3.GetOutPorts().
            |       ...
            + "SomeNode" (GraphNode): The next tangible node in the graph data structure, it also does
            |   |                     store its data in the same way as "Material3". In general, all
            |   |                     "true" nodes are stored in this flat manner, with the exception
            |   |                     of node groups.
            |   + ...
            ...

    Args:
        node (nodes.GraphNode): The node to start the search with.
        nid (maxon.Id): The node ID to look for.
        kind (maxon.ID, optional): The node type to limit the search to. Defaults to None.

    Returns:
        typing.Optional[nodes.GraphNode]: The found node or None.
    """
    if not isinstance(node, nodesAPI.GraphNode) or not isinstance(nid, maxon.Id):
        return None
    
    # Test if the node is an ID and optionally node kind match.
    isIdMatch: bool = node.GetId() == nid
    if isIdMatch and not isinstance(kind, maxon.Id):
        return node
    elif isIdMatch and node.GetKind() == kind:
        return node

    # Start the recursion, for bonus points, one could implement this iteratively.
    for child in node.GetChildren():
        result = FindNode(child, nid)
        if result:
            return result

    # Just for verbosity here.
    return None

def PrintTree(node: nodesAPI.GraphNode, indent: int = 0) -> None:
    """Prints a tree of graph elements.
    """
    space = " " * indent
    memLoc: str = f"0x{hex(id(node)).upper()[2:].zfill(16)}"
    print (f"{space}'{node.GetId()}'(kind = {node.GetKind()} at {memLoc})")
    
    for child in node.GetChildren():
        PrintTree(child, indent + 1)

def main():
    """Runs the example.
    """
    # Your code ...
    mat: c4d.BaseMaterial = doc.GetActiveMaterial()
    if mat is None:
        return

    mat: c4d.NodeMaterial = mat.GetNodeMaterialReference()
    if mat is None:
        return
        
    graphHandler: maxon.NimbusBaseRef = mat.GetNimbusRef(c4d.GetActiveNodeSpaceId())
    graph: nodesAPI.NodesGraphModelRef = graphHandler.GetGraph()

    # Get the root node of the graph.
    root: nodesAPI.GraphNode = graph.GetRoot()

    # Get the first element with the ID "material".
    material = FindNode(root, maxon.Id("material"))

    # Get the first element with the ID "bsdflayer", this will work here, since there is only one
    # element in my example scene which uses this ID.
    bsdfLayer = FindNode(root, maxon.Id("bsdflayer"))

    # A bit more safe, search for the first nested element with the ID "bsdflayer" below #material.
    alsoBsdfLayer = FindNode(material, maxon.Id("bsdflayer"))

    # Even more safe, search for the first nested element with the ID "bsdflayer" below #material
    # that is an input-port.
    alsoalsoBsdfLayer = FindNode(material, maxon.Id("bsdflayer"), graphAPI.NODE_KIND.INPORT)
    
    # Print the nodes, printing nodes will utilize their NodePath, i.e., print (node) is equal to
    # print (node.GetPath())
    print (f"{root = }")
    print (f"{material = }")
    print (f"{bsdfLayer = }")
    print (f"{alsoBsdfLayer = }")
    print (f"{alsoalsoBsdfLayer = }")
    print (f"{alsoalsoBsdfLayer.GetPath() = }\n\n")
    
    # Print the tree structure with which elements of a NodeGraph are stored.
    print (f"The node graph for {graph}:")
    PrintTree(root)

if __name__ == "__main__":
    main()

@ferdinand

Thanks for the sample code and explanation of the limitation.
The code works as expected for this part
doc: c4d.documents.BaseDocument # The active document.
I'm not too familiar with the Python 3 syntax but this line returns an empty graph down the line.
Deleted it and it works as expected.

And yea that Graph Node classification is a misnomer.
Calling an inport and outport a type of GraphNode when it is clearly not is a bit confusing.

Hello @bentraje

@bentraje said in How to Use ListAllNodes?:

doc: c4d.documents.BaseDocument # The active document.
I'm not too familiar with the Python 3 syntax but this line returns an empty graph down the line.

Tha is a bit odd, especially since I tested and ran the code with R25. This line or code such as below

# Without type hinting.
def foo(a):
    return f"{a}"

# With type hinting.
def foo(a: typing.Any) -> str:
    return f"{a}"

makes use of type hinting. It tries to rectify ambiguities that come with the fact that Python is dynamically typed. For now, type hinting has no real impact on code run on CPython (i.e., the "standard" Python), unless you use special libraries to run your CPython code or use interpreters other than CPython which support static or at least asserted typing out of the box.

All the line

doc: c4d.documents.BaseDocument # The active document.

does for now is:

  1. Make code more readable by telling humans what the module attribute doc is and that it exists in the first place.
  2. Enable auto-complete engines as provided by, for example, VSCode to give you smart suggestions.
  3. Enable linters or runtime libraries to run type assertions on your code.

Adding or removing this line should have zero impact on the execution, unless you added there an equals sign and overwrote the value of doc.

Cheers,
Ferdinand