traversal

Demonstrates scene traversal.

traversal.py

"""Demonstrates scene traversal.

Topics:
    * Safely navigating a node hierarchy
    * Traversing node trees in different orders
    * Traversing hierarchies, caches, and branches as a graph
    * Filtering graph traversal by branch and node type
"""
__author__ = "Ferdinand Hoppe"
__copyright__ = "Copyright (C) 2026 MAXON Computer GmbH"
__license__ = "Apache-2.0 License"
__version__ = "2027.0.0"
__date__ = "07/09/2026"

import c4d
import mxutils


doc: c4d.documents.BaseDocument # The active Cinema 4D document.
op: c4d.BaseObject | None # The currently selected object in the Cinema 4D document, can be None.


def main() -> None:
    """Runs the example.
    """
    def Callback(node: c4d.BaseObject) -> None:
        """Unfolds the objects added by AddHierarchyFromString and adds tags to them.
        """
        node.ChangeNBit(c4d.NBIT_OM1_FOLD, c4d.NBITCONTROL_SET)
        node.MakeTag(c4d.Tdisplay)
        if node.GetType() in (c4d.Osphere, c4d.Ocone, c4d.Ocylinder):
            tag: c4d.BaseTag = mxutils.CheckType(node.MakeTag(c4d.Tphong))
            tag[c4d.PHONGTAG_PHONG_ANGLELIMIT] = True

    print("\n--- Setup -------------------------------------------------------------------------\n")

    # Build the hierarchy used by the traversal examples.
    #
    #    A
    #    ├─ B
    #    │  ├─ C
    #    │  ├─ D
    #    │  │  └─ E
    #    │  └─ F
    #    G
    #    └─ H
    #
    doc.Flush()
    objects: list[c4d.BaseObject] = mxutils.g_scene_faker.AddHierarchyFromString(
        doc,
        f"""
        A
            B::{c4d.Ocube}
                C::{c4d.Osphere}
                D::{c4d.Ocone}
                    E::{c4d.Osphere}
                F::{c4d.Ocylinder}
        G
            H
        """,
        callback=Callback
    )

    # Add random layers, render data, materials, and tracks to the scene.
    mxutils.g_scene_faker.RandomizeTransform(
        objects, minPosition=c4d.Vector(-500), maxPosition=c4d.Vector(500))
    mxutils.g_scene_faker.AddRandomLayers(doc, minCount=2, maxCount=2)
    mxutils.g_scene_faker.AddRandomRenderData(doc, minCount=2, maxCount=2)
    mxutils.g_scene_faker.AddRandomMaterials(doc, minCount=5, maxCount=5, objects=objects)
    mxutils.g_scene_faker.AddRandomTracks(objects, minCount=2, maxCount=2, minKeys=2, maxKeys=2)

    rData: c4d.documents.RenderData = mxutils.CheckType(doc.GetActiveRenderData())
    rData[c4d.RDATA_RENDERENGINE] = c4d.RDATA_RENDERENGINE_STANDARD

    print("\n--- Getting Nodes -----------------------------------------------------------------\n")

    # GetNodeFromPath() is the recommend way to retrieve nodes by a hierarchical path which is deeper
    # than one level. Chaining multiple calls to GetDown(), GetNext(), etc., will otherwise lead to
    # cryptic runtime errors when a path does not exist.

    # Get the first object in the document, which is "A".
    a: c4d.BaseObject | None = mxutils.CheckType(doc.GetFirstObject())
    if a is None:
        raise ValueError("The first object in the document is not found.")

    # Get the node down from "A", which is "B". We could also simply write a.GetDown() in this case
    # and it would be functionally equivalent and also slightly faster.
    b: c4d.BaseObject | None = mxutils.GetNodeFromPath(a, "d")
    
    # Get the down, down, next, down node from "A", which is "E" (A -> B -> D -> E). For a failing
    # path the function will simply return None. You can also pass the last argument raisePathErrors
    # as True to raise an error if the path does not exist.
    e: c4d.BaseObject | None = mxutils.GetNodeFromPath(a, "ddnd") #

    # More of the same, they are needed for examples below.
    f: c4d.BaseObject | None = mxutils.GetNodeFromPath(a, "ddnn")
    h: c4d.BaseObject | None = mxutils.GetNodeFromPath(a, "nd")
    
    print(f"GetNodeFromPath(a, 'd') = {b}")
    print(f"GetNodeFromPath(a, 'ddnd') = {e}")
    print(f"GetNodeFromPath(a, 'ddnn') = {f}")
    print(f"GetNodeFromPath(a, 'nd') = {h}")

    # The function GetRootNode() retrieves the top-most ancestor of the given node (its root). You
    # can also use GetNodeFromPath(node, "r") to achieve the same result.
    rootE: c4d.BaseObject = mxutils.GetRootNode(e)
    rootH: c4d.BaseObject = mxutils.GetNodeFromPath(h, "r")
    print(f"GetRootNode(e) = {rootE}")
    print(f"GetNodeFromPath(h, 'r') = {rootH}")

    # The function GetFirstSiblingNode() retrieves the first sibling of the given node. I.e., the
    # first child of the parent of the given node (or the first node in the branch when there is no 
    # parent).
    firstSiblingF: c4d.BaseObject = mxutils.GetFirstSiblingNode(f)
    firstSiblingB: c4d.BaseObject = mxutils.GetNodeFromPath(b, "f")
    print(f"GetFirstSiblingNode(f) = {firstSiblingF}")
    print(f"GetNodeFromPath(b, 'f') = {firstSiblingB}")

    # GetNodeFromPath() supports the following traversal symbols:
    #
    #   "d" - down (first child)
    #   "u" - up (parent)
    #   "n" - next (next sibling)
    #   "p" - previous (previous sibling)
    #   "f" - first sibling
    #   "r" - root
    
    # A -> B -> C -> D -> F -> D -> B
    b: c4d.BaseObject | None = mxutils.GetNodeFromPath(a, "ddnnpu")
    print(f"GetNodeFromPath(a, 'ddnnpu') = {b}")

    print("\n--- Traversing Trees --------------------------------------------------------------\n")

    # With the function IterateTree(), we can walk arbitrary GeListNode trees. We do it here only 
    # with objects, but it would also work the same with documents, tags, materials, etc.

    # Walk over all objects in the local hierarchy of A. This will ignore G and H, as they are not
    # direct descendants of A.
    print("\nmxutils.IterateTree(a)")
    for item in mxutils.IterateTree(a):
        print(f"\t{item.GetName()}")

    # Walk over all objects starting from A, including those that come 'after' A (G and H).
    print("\nmxutils.IterateTree(a, yieldSiblings=True)")
    for item in mxutils.IterateTree(a, yieldSiblings=True):
        print(f"\t{item.GetName()}")

    # IterateTree offers also some of the rewinding functionality we have seen before which makes
    # it easier to traverse a tree when starting from an arbitrary node. This exact call below will
    # always yield the full tree, no matter from where we start. The rewinding is done before the
    # traversal begins and #rewindToRoot takes precedence over #rewindToFirstSibling.
    print("\nmxutils.IterateTree(h, yieldSiblings=True, rewindToFirstSibling=True, rewindToRoot=True)")
    for item in mxutils.IterateTree(h, yieldSiblings=True, rewindToFirstSibling=True, rewindToRoot=True):
        print(f"\t{item.GetName()}")

    # Finally, IterateTree offers three traversal modes. Our tree looks like this:
    #
    #    A
    #    ├─ B
    #    │  ├─ C
    #    │  ├─ D
    #    │  │  └─ E
    #    │  └─ F
    #    G
    #    └─ H
    #
    # The traversal modes are:
    #
    #   1. Depth-First: This is the default and will yield nodes as a human would read them, top-down
    #      and left-to-right. I.e., A, B, C, D, E, F, G, H in this case.
    #
    #   2. Inverse Depth-First: In this mode, children are yielded before their parents. This can be 
    #      useful for operations where you have to work 'from the inside out', such as deleting 
    #      elements or flattening a hierarchy. Will yield in this case: C, E, D, F, B, A, H, G.
    #
    #   3. Breadth-First: This will yield all siblings of a node before descending into their 
    #      children. You can also think of this mode as yielding in columns, (A, G) is the first 
    #      column, (B, H) is the second column, (C, D, F) is the third column, and (E) is the fourth 
    #      column. This can be useful for operations that need to process all nodes at a given depth 
    #      before moving deeper into the hierarchy. Will yield A, G, B, H, C, D, F, E in this case.

    print("\nmxutils.IterateTree(a, yieldSiblings=True, traversalMode=mxutils.TRAVERSAL_DEPTH_FIRST)")
    for item in mxutils.IterateTree(a, yieldSiblings=True, traversalMode=mxutils.TRAVERSAL_DEPTH_FIRST):
        print(f"\t{item.GetName()}")

    print("\nmxutils.IterateTree(a, yieldSiblings=True, traversalMode=mxutils.TRAVERSAL_INVERSE_DEPTH_FIRST)")
    for item in mxutils.IterateTree(a, yieldSiblings=True, traversalMode=mxutils.TRAVERSAL_INVERSE_DEPTH_FIRST):
        print(f"\t{item.GetName()}")

    print("\nmxutils.IterateTree(a, yieldSiblings=True, traversalMode=mxutils.TRAVERSAL_BREADTH_FIRST)")
    for item in mxutils.IterateTree(a, yieldSiblings=True, traversalMode=mxutils.TRAVERSAL_BREADTH_FIRST):
        print(f"\t{item.GetName()}")

    print("\n--- Traversing Graphs -------------------------------------------------------------\n")

    # Until now we have only dealt with the main hierarchical structure of the document, i.e., the
    # parent-child relationships between nodes. But Cinema also provides two other types of 
    # relationships: caches and branches.
    #
    # What are Caches?
    #
    # Caches are a special type of relationship of generator objects. When you have for example a
    # Cube object generator in a scene, it will have a static cache which is the PolygonObject
    # representation of the generator's output. There are also deform caches which store the deformed 
    # state of an object. Caches can also consist out of multiple objects and contain caches 
    # themselves (a generator that returns another generator as its cache). See 
    # modeling/geometry_cache.py for more details.
    #
    # What are Branches?
    #
    # Branches are Cinema's twist on arbitrary relationships between nodes. Each GeListNode can have
    # in addition to its hierarchical relationships arbitrary relations to other nodes. A document
    # has for example an 'Objects' branch which holds the the hierarchy of all objects, and a 
    # 'Materials' branch which holds the hierarchy of all materials. An object in turn has for 
    # example a 'Tag' branch which holds all its tags and a 'Tracks' branch which holds all its 
    # tracks. And tracks have ten curve branches, etc. In this manner, Cinema 4D realizes what is
    # commonly referred to as a "scene graph".

    a: c4d.BaseObject | None = doc.GetFirstObject()

    # When we only pass a node to IterateGraph without any additional parameters, nothing will happen
    # because all traversal options are disabled by default.
    print("\nmxutils.IterateGraph(a):")
    for node in mxutils.IterateGraph(a):
        print(f"\t{node}")

    # This call will yield all hierarchically related nodes starting from 'a'. Note that other than in
    # IterateTree(), there is no option to also consume next siblings of the starting node. If we wanted
    # to yield all objects, we would have to start from the document instead.
    print("\nmxutils.IterateGraph(a, yieldHierarchy=True):")
    for node in mxutils.IterateGraph(a, yieldHierarchy=True):
        print(f"\t{node}")

    # But we can now also do something different and also yield the caches of our objects, i.e., the
    # internal state of generator objects, in this case the `PolygonObject` instances that the 
    # generators built we added to the scene.
    print("\nmxutils.IterateGraph(a, yieldHierarchy=True, yieldCaches=True):")
    for node in mxutils.IterateGraph(a, yieldHierarchy=True, yieldCaches=True):
        print(f"\t{node}")

    # And finally, we can also include branches in our traversal. Which means that we will also see
    # tags, tracks, curves , and possibly other node types being yielded, as they are in a branching
    # relation with our objects. Note that both caches and branches are internally also hierarchical.
    # So, unless we only want to see the first node of a brach or cache, we usually want to enable
    # both hierarchical traversal and branch/cache traversal. Caches and branches are mostly independent 
    # of each other but branches can contain caches and caches can contain branches. But usually cache
    # traversal is not so important and can be left disabled.
    print("\nmxutils.IterateGraph(a, yieldHierarchy=True, yieldCaches=True, yieldBranches=True):")
    for node in mxutils.IterateGraph(a, yieldHierarchy=True, yieldCaches=True, yieldBranches=True):
        print(f"\t{node}")

    # Besides the normal node-only yield type, mxutils.IterateGraph also has a complex yield type that
    # provides more information about the yielded node.
    print("\nmxutils.IterateGraph(a, yieldBranches=True, yieldHierarchy=True, complexYield=True):")
    for node, owner, depth, isBranch, isCache in mxutils.IterateGraph(
        a, yieldBranches=True, yieldHierarchy=True, complexYield=True):
        print(f"\t{node =}, {owner =}, {depth =}, {isBranch =}, {isCache =}")

    # As discussed before, documents are also nodes, so we can also start traversal from a document.
    # That is in fact one of the most common forms of traversal.

    # We collect literally ALL nodes in the document, this can be an expensive operation for very 
    # large scenes which might run for multiple milliseconds (e.g., a 5000 node scene takes on 
    # average 20ms on my machine).  Our scene will contain only 236 nodes (at the time of writing,
    # this can change).
    nodes: list[c4d.BaseList2D] = list(mxutils.IterateGraph(
        doc, yieldHierarchy=True, yieldCaches=True, yieldBranches=True))
    print(f"The document {doc} contains {len(nodes)} nodes.")

    # To sort out all this information, IterateGraph offers multiple filtering operations. See the
    # docs for in detail information on them. One of the most important filters is the branchFilter. 
    # It determines from which brach types the function will yield nodes. By default it DOES NOT 
    # mean that it will not step into other branches, it just does not yield from them.

    # Iterate over all nodes in the document, but only yield from Obase and Tbase branches, i.e.,
    # object and tag branches.
    print("\nmxutils.IterateGraph with branchFilter=[c4d.Obase, c4d.Tbase]:")
    for node in mxutils.IterateGraph(
        doc, yieldHierarchy=True, yieldBranches=True, branchFilter=[c4d.Obase, c4d.Tbase]):
        print(f"{node = }")

    # This is complemented by the argument stopBranchingEarly. This will cause branchFilter not only
    # to act as a yield but also traversal filter. The function will now only step into branches of
    # the given type. This can be a massive performance improvement for large scenes, but we must 
    # include all necessary branch types to reach our desired nodes. As an example, if we would only 
    # pass Tbase, and use a document as the starting node, we would never see any output, as tags 
    # live in objects, and to get there, the function would have to first step into the Obase branch 
    # of a document.
    
    # This cannot reach anything, as documents do not have tag branches.
    print("\nmxutils.IterateGraph with branchFilter=[c4d.Tbase] and stopBranchingEarly=True:")
    for node in mxutils.IterateGraph(
            doc, yieldHierarchy=True, yieldBranches=True, branchFilter=[c4d.Tbase], stopBranchingEarly=True):
            print(f"{node = }")

    # Another filter type is nodeFilter which lets us filter nodes based on type. We can either use
    # specific types such as Ocube or base types such as Obase (which would fit all objects).

    # Find all cube objects and any form of tags in the scene, restricted to the the branch path
    # (Obase, Tbase).
    print("\nmxutils.IterateGraph with branchFilter=[c4d.Obase, c4d.Tbase], "
          "stopBranchingEarly=True, and nodeFilter=[c4d.Ocube, c4d.Tbase]:")
    for node in mxutils.IterateGraph(
        doc, yieldHierarchy=True, yieldBranches=True, branchFilter=[c4d.Obase, c4d.Tbase], 
        stopBranchingEarly=True, nodeFilter=[c4d.Ocube, c4d.Tbase]):
        print(f"{node = }")

    # The last filter type is maxBranchingDepth, which allows us to limit how deep the traversal goes 
    # into the branch hierarchy. Just as with stopBranchingEarly, this can help optimize traversal by
    # avoiding unnecessary exploration of deep branches.

    # We look in a document for all nodes of type tag with a maximum branching depth of 1. As in the
    # previous example, this can never yield anything, because tags sit at least two branches levels
    # below a document (Objects > Tags).
    print("\nmxutils.IterateGraph with maxBranchingDepth=1 and nodeFilter=[c4d.Tbase]:")
    for node in mxutils.IterateGraph(
        doc, yieldHierarchy=True, nodeFilter=[c4d.Tbase], maxBranchingDepth=1):
        print(f"{node = }")

    print("\n--- Manual Branch Traversal ---------------------------------------------------------")

    # NOTE: The information provided below is expert knowledge and most users can ignore it. But
    # when you want to implement a plugin which holds custom branches (via NodeData::GetBranchInfo),
    # you should understand this.

    # Branches are reachable via the function GetBranchInfo. Here we get the branches for a document.
    branches: list[dict[str, any]] = doc.GetBranchInfo(c4d.BRANCHINFOFLAGS_NONE) or []
    print(f"{branches = }")

    # Branches are practically realized with the type c4d.GeListHead (a subtype of GeListNode), which 
    # serves as the root for a branch. So, we have a tree GeListNode's (for example an object tree),
    # where each node points to a couple of GeListHeads (its branches), which then hold the hierarchy 
    # of nodes within that branch.

    # Get the the branch head for the branch with the id Obase in the document: The object branch.
    objectHead: c4d.GeListHead | None = next(
        (n.get("head") for n in branches if n.get("id") == c4d.Obase), None)

    # Get the the branch head for the branch with the id Mbase in the document: The material branch.
    materialHead: c4d.GeListHead | None = next(
        (n.get("head") for n in branches if n.get("id") == c4d.Mbase), None)

    # There also exists 'name' and 'flags' metadata for each branch, which is being ignored here.

    print(f"{objectHead = }")
    print(f"{materialHead = }")

    # Since branch heads are also c4d.GeListNode, we can use the traversal methods we already know.
    print(f"\nIterating over the object branch:")
    for node in mxutils.IterateTree(objectHead):
        print(f"{node = }")

    print(f"\nIterating over the material branch:")
    for node in mxutils.IterateTree(materialHead):
        print(f"{node = }")

    # You can get the GeListHead for given node with GetListHead.
    first: c4d.BaseObject | None = objectHead.GetDown()
    second: c4d.BaseObject | None = first.GetNext()

    firstHead: c4d.GeListHead | None = first.GetListHead()
    secondHead: c4d.GeListHead | None = second.GetListHead()
    print(f"\n{firstHead = }")
    print(f"{secondHead = }")
    print(f"{firstHead == secondHead}") # They are the same because both nodes belong to the same branch.

    # You can get the owner, i.e., the node that holds the branch of a given node, with GetMain().
    firstMain: c4d.GeListNode | None = first.GetMain() if first else None
    seconmdMain: c4d.GeListNode | None = second.GetMain() if second else None
    print(f"\n{firstMain = }")
    print(f"{seconmdMain = }")

    c4d.EventAdd()


if __name__ == "__main__":
    c4d.ClearPythonConsole()
    main()