SOLVED Make Node Callbacks Optional?

In the method, GetSelectedNodes(graphModel, kind, callback) it requires a callback function.
Is there a way to make that optional?

I just want to store some nodes like a list like what it should return and do the processing later.

Is this possible?

Hello @bentraje,

I assume you are on R25 again, and are talking about GraphModelInterface.GetSelectedNodes. In 2023.1, this method has been moved to GraphModelHelper.GetSelectedNodes and there were also some changes with these helper methods.

General maxon API Architecture

I assume you want something like myData: list[maxon.GraphNode] = soemthing.SomeMethod(*data). This is however not how C++ often works and our C++ APIs in particular, as returning a value often means copying it (which you want to avoid). Therefore, developers of rely on out arguments. So, instead of doing this:

maxon::BaseArray<Int32> data = soemthing.A();

you do this:

maxon::BaseArray<Int32> data;
soemthing.B(data);

A and B do the same thing, but B uses an out argument instead of returning its result. This is then reflected in the Python API which can feel a bit alien there. It is also extended in the maxon API by the type ValueReceiver which is a handler for a container of values of the same type. This handler can either be a collection, e. g., maxon::BaseArray<Int32>, or a delegate, a callback function.

GetSelectedNodes and the Argument callback

Looking at the R25 and 2023.1 docs, the documentation of the method seems to be incorrect in both cases. The R25 docs look like this:

b06c0f3f-1a5a-4126-ae0b-0a0ff8859ef4-image.png

The line:

callback: Union[[maxon.frameworks.graph.GraphNode], Callable(maxon.frameworks.graph.GraphNode)])

does not make too much sense and is invalid typing markup. The author could have meant that you can either pass a list which will be populated with GraphNode or a callable which will receive graph nodes. I.e., this, the signature of a ValueReceiver:

callback: Union[list[GraphNode], Callable[[GraphNode], bool])

The 2023.1 docs are better but also not correct:

callback (Callable[[maxon.GraphNode], bool]) – ...

This should also be:

callback: Union[list[GraphNode], Callable[[GraphNode], bool])

I have written a short example which demonstrates value receivers at the example of GetSelectedNodes. Currently you must look at GetSelectedNodes in the C++ docs to understand how this works, we will fix the Python docs in an upcoming release.

R25, GetSelectedNodes and Return Values

R25 is out of scope of SDK support, and you must check yourself. It seems likely that callback there does also accept a list of nodes as the callback argument. But I cannot guarantee that because this does not work automatically and must be handled manually in the Python wrapper because list is not a native maxon API type. In general, the Nodes API was in R25 it is infancy.

It is also unlikely that we will move away from this out argument approach in the Python (maxon) API , primarily from a willingness standpoint. Because we strive for signature parity between C++ and Python wherever it is possible.

Cheers,
Ferdinand

The result run on a standard node material in 2023.1:

bsdf
material
bsdf<roughness
bsdf<normal
bsdf<normalstrength
------------------------------
bsdf
bsdf<roughness

The code:

"""Demonstrates the usage of ValueReceivers.
"""

import c4d
import maxon
import typing

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

def check(item: typing.Any) -> typing.Any:
    """Asserts that an item is not None.

    The item is passed through on success.
    """
    if item is None:
        raise MemoryError(f"{item = }")
    return item

def main():
    """
    """
    # Get the active material in the active document.
    material: c4d.BaseMaterial = check(doc.GetActiveMaterial())

    # Retrieve the graph for the active node space.
    nodeSpaceId: maxon.Id = c4d.GetActiveNodeSpaceId()
    graphHandler: maxon.NimbusBaseRef = check(material.GetNimbusRef(nodeSpaceId))
    graph: maxon.GraphModelRef = check(graphHandler.GetGraph())


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

    # Search for all selected nodes using a collection, in Python a list, as the value receiver.
    result: list[maxon.GraphNode] = []
    maxon.GraphModelHelper.GetSelectedNodes(graph, maxon.NODE_KIND.ALL_MASK, result)
    for item in result:
        print (item)

    # Do the same but using a "true" delegate, here we have more control over the outcome and can
    # stop the search early.
    print ("-" * 30)
    def myReceiver (node: maxon.GraphNode) -> bool:
        """The callback function which implements a value receiver.

        A value receiver receives an item from a collection and can either return #True to stop
        the search or #False to continue it. This has the advantage that when something contains
        1000 things which are time-consuming to find, and you are only interested in the third item,
        that the execution will be much shorter.

        For the "result: list[maxon.GraphNode] = []" approach, you will retrieve all 1000 items, 
        then iterate over them, find out that the third item was what you wanted and throw away the
        rest. With the true delegate approach, you will only visit the first three items.
        """
        # Print the node.
        print (node)

        # Stop the search when the node was any kind of port.
        if node.GetKind() == maxon.NODE_KIND.PORT_MASK:
            return True
        
        # Otherwise continue searching.
        return False

    # Will also traverse all selected nodes, but will stop early once it does find a port.
    maxon.GraphModelHelper.GetSelectedNodes(graph, maxon.NODE_KIND.ALL_MASK, myReceiver)

    

if __name__ == "__main__":
    main()

@ferdinand

Thanks as always for the explanation.
The illustration code works for my use case.
Will close now thread 🙂