UNSOLVED Can I get active things order with python?

Hello :

Question :

I want to get a order of what I select and activate last . Can I do this wth python ?

More Descriptions :

For example , I select some thing and they can exist active at the sametime. e.g.

  • object A
  • tag A on object B
  • Mat A in material manager
  • layer B in layer manager .
  • even a attribute in Atrribute Manager

If I want a quick function for example reneme . I don't want to GetActiveObjects scripts for Object Manager and another GetActiveMaterials scripts for Material Manager , that means to many scripts with a same task .

Instead , I want only one script that can judge which is my last select thing or which context my mouse in , so that I can run a corresponding function . ( even more like execute on tags and both on materials )

Can or how can I do this with python ?

Here is a picture for better explain what I mean active at same time .
ab2eca97-20c4-474c-96e3-3dfde4c35a59-image.png

Hello @dunhou,

Thank you for reaching out to us. The answer to your question is "sort of', especially since it implies a follow up question 😉

In the front end there are several convenience methods with which you can retrieve selected things in a document, you already know them, e.g.:

  • c4d.BaseDocument.GetActiveObjects(flags)
  • c4d.BaseDocument.GetActiveMaterials()
  • c4d.BaseDocument.GetActiveTags()
  • ...

With .GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER) you can also preserve the selection order of objects in the list returned by that method.

  • There are however no equivalents for the selection order for materials, tags, etc.
  • There also do not exist methods for all scene element types which are selectable, and you sometimes you must iterate yourself over elements to find out if they are selected or not.
  • The selection state of a node is expressed in the flag c4d.BIT_ACTIVE, i.e., someNode.GetBit(c4d.BIT_ACTIVE) will return true when that node is selected.
  • The selection state of parameters in the Attribute Manger is not accessible from the public API.

Cinema 4D does manage the content of a document as any 3D DCC app in some notion of a scene graph. In the Cinema 4D classic API this is primarily realized with the type c4d.GeListNode and the concept of branches as embodied by the type c4d.GeListHead and the method c4d.GeListNode.GetBranchInfo.

With the help of c4d.GeListNode.GetBranchInfo you can fashion yourself a function with which you can iterate abstractly over a document, i.e., have something like this:

for node in IterateGraph(doc, [c4d.Obase, c4d.Tbase, c4d.Mbase, c4d.Olayer]):

where then the function will yield all objects, tags, materials, and layers in doc. This touches on the non-trivial topic of scene traversal. We are aware that we should provide convenience functions for that, but it has not been done yet. Find below a Python script which highlights a simple implementation of such IterateGraph function.

Cheers,
Ferdinand

The example scene:
example_scene.c4d
500588b8-4923-4520-9ada-78c391359407-image.png

The output for the example scene, with some comments:


# We get both layers from doc, the second one is selected.
IterateGraph(doc, [c4d.Olayer])
    LayerObject 'Layer' at 0x000001519703F5C0 (isActive = False)
    LayerObject 'Layer 1' at 0x0000015197040880 (isActive = True)

# We get all cube objects and all tags in the scene.
IterateGraph(doc, [c4d.Ocube, c4d.Tbase])
    # Tags of the sphere object, the object itself is not included because it is not Ocube
    BaseTag 'Phong' at 0x00000151970300C0 (isActive = False)
    TextureTag 'Material' at 0x000001519702B580 (isActive = False)
    # The cube objects and their tags.
    BaseObject 'Cube' at 0x00000151970345C0 (isActive = True)
    TextureTag 'Material' at 0x0000015197009680 (isActive = False)
    BaseObject 'Cube.1' at 0x0000015197041B80 (isActive = False)
    BaseTag 'Phong' at 0x0000015197013E80 (isActive = False)
    BaseTag 'Look at Camera' at 0x000001519703D4C0 (isActive = False)
    TextureTag 'Material' at 0x000001519702CFC0 (isActive = False)

# The generator object "Cube" has no phong tag, but a PolygonObject cache which we unpack here.
IterateGraph(cube, [c4d.Obase, c4d.Tphong], inspectCaches=True)
    BaseObject 'Cube' at 0x0000015197006F00 (isActive = True)
    PolygonObject 'Cube' at 0x0000015114952980 (isActive = False)

# This yields nothing although there is a override in the scene, because we never go in the
# object branches here (where the override is located in this case)
IterateGraph(doc, [c4d.OverrideBase])

# This works :)
IterateGraph(doc, [c4d.Obase, c4d.OverrideBase])
    BaseObject 'Sphere' at 0x000001519701D2C0 (isActive = False)
    BaseObject 'Cube' at 0x0000015197037AC0 (isActive = True)
    BaseObject 'Cube.1' at 0x0000015197021DC0 (isActive = False)
    BaseOverride 'Override' at 0x0000015197012B80 (isActive = False)
    BaseOverride 'Override' at 0x0000015197026F80 (isActive = True)

# Your case plus some description inspection for parameters.
IterateGraph(doc, [c4d.Obase, c4d.Tbase, c4d.Mbase, c4d.Olayer])
    BaseObject 'Sphere' at 0x000001519702B840 (isActive = False)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 15, 110050)): 0
        ...
    BaseTag 'Phong' at 0x000001519703C1C0 (isActive = False)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 400006001, 110050)): 0
        ...
    TextureTag 'Material' at 0x000001519703F3C0 (isActive = False)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 400006001, 110050)): 0
        ...
    BaseObject 'Cube' at 0x0000015197031780 (isActive = True)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 15, 110050)): 0
        ...
    TextureTag 'Material' at 0x000001519702FA80 (isActive = False)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 400006001, 110050)): 0
        ...
    BaseObject 'Cube.1' at 0x0000015197023B80 (isActive = False)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 15, 110050)): 0
        ...
    BaseTag 'Phong' at 0x0000015197010BC0 (isActive = False)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 400006001, 110050)): 0
        ...
    BaseTag 'Look at Camera' at 0x000001519701FDC0 (isActive = False)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 400006001, 110050)): 0
        ...
    TextureTag 'Material' at 0x000001519702FE00 (isActive = False)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 400006001, 110050)): 0
        ...
    Material 'Red' at 0x000001519701B1C0 (isActive = False)
        Parameter ''(descId = (831, 1, 1001065)): None
        Parameter ''(descId = (536871064, 1, 1001065)): None
        Parameter ''(descId = (520000000, 1011153, 1001065)): Not accessible
        Parameter ''(descId = (832, 12, 1001065)):
        ...
    Material 'Green' at 0x0000015197010440 (isActive = True)
        Parameter ''(descId = (831, 1, 1001065)): None
        Parameter ''(descId = (536871064, 1, 1001065)): None
        Parameter ''(descId = (520000000, 1011153, 1001065)): Not accessible
        Parameter ''(descId = (832, 12, 1001065)):
        ...
    Material 'Blue' at 0x000001519703DD80 (isActive = False)
        Parameter ''(descId = (831, 1, 1001065)): None
        Parameter ''(descId = (536871064, 1, 1001065)): None
        Parameter ''(descId = (520000000, 1011153, 1001065)): Not accessible
        Parameter ''(descId = (832, 12, 1001065)):
        ...
    LayerObject 'Layer' at 0x00000151970098C0 (isActive = False)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 400006001, 110050)): 0
        ...
    LayerObject 'Layer 1' at 0x0000015197029A40 (isActive = True)
        Parameter 'Basic Properties'(descId = (110050, 1, 110050)): None
        Parameter 'Icon'(descId = (1041666, 1, 110050)): None
        Parameter 'Icon File / ID'(descId = (1041668, 7, 110050)):
        Parameter 'Icon Color'(descId = (1041670, 400006001, 110050)): 0
        ...

# We can also just look at everything in a document when passing no type list.
# As you can see, there are a lot of nodes in a scene, even when it seems
# almost empty, most of them are scene hooks.
IterateGraph(doc)
    BaseDocument '' at 0x0000015114951C80 (isActive = True)
    BaseObject 'Sphere' at 0x000001519700B3C0 (isActive = False)
    BaseTag 'Phong' at 0x000001519701B4C0 (isActive = False)
    TextureTag 'Material' at 0x0000015197023A00 (isActive = False)
    BaseObject 'Cube' at 0x0000015197024080 (isActive = True)
    TextureTag 'Material' at 0x00000151970417C0 (isActive = False)
    BaseObject 'Cube.1' at 0x0000015197024540 (isActive = False)
    BaseOverride 'Override' at 0x00000151970246C0 (isActive = False)
    BaseOverride 'Override' at 0x000001519702BA00 (isActive = True)
    BaseTag 'Phong' at 0x0000015197020B00 (isActive = False)
    BaseTag 'Look at Camera' at 0x000001519700CEC0 (isActive = False)
    TextureTag 'Material' at 0x0000015197011540 (isActive = False)
    Material 'Red' at 0x000001519701A740 (isActive = False)
    Material 'Green' at 0x0000015197016EC0 (isActive = True)
    Material 'Blue' at 0x00000151970394C0 (isActive = False)
    RenderData 'My Render Setting' at 0x00000151970148C0 (isActive = False)
    BaseVideoPost 'Magic Bullet Looks' at 0x0000015197031540 (isActive = False)
    LayerObject 'Layer' at 0x000001519701A2C0 (isActive = False)
    LayerObject 'Layer 1' at 0x0000015197029780 (isActive = True)
    BaseList2D 'USD Scene Hook' at 0x0000015197025780 (isActive = False)
    BaseList2D 'Substance Assets' at 0x00000151970223C0 (isActive = False)
    BaseList2D 'STHOOK' at 0x000001519703BCC0 (isActive = False)
    BaseList2D 'SceneHook' at 0x0000015197014140 (isActive = False)
    BaseList2D 'CmSceneHook' at 0x0000015197004600 (isActive = False)
    BaseList2D 'CameraMorphDrawSceneHook' at 0x0000015197012F00 (isActive = False)
    BaseList2D 'MotionCameraDrawSceneHook' at 0x00000151970291C0 (isActive = False)
    BaseList2D 'UpdateMerge Hook' at 0x0000015197008000 (isActive = False)
    BaseList2D 'ArchiExchangeCADHook' at 0x000001519701B680 (isActive = False)
    BaseList2D 'Alembic Archive Hook' at 0x00000151970146C0 (isActive = False)
    BaseList2D 'SLA wave scene hook' at 0x0000015197002B80 (isActive = False)
    TP_MasterSystem 'Thinking Particles' at 0x000001519703D600 (isActive = False)
    BaseList2D 'Bullet' at 0x0000015197002F00 (isActive = False)
    BaseList2D 'XRefs' at 0x000001519702A700 (isActive = False)
    BaseList2D 'CAManagerHook' at 0x0000015197030540 (isActive = False)
    BaseList2D 'Weights Handler' at 0x0000015197007500 (isActive = False)
    BaseList2D 'Volume Save Manager Hook' at 0x00000151970404C0 (isActive = False)
    BaseList2D 'UV Display 3D SceneHook' at 0x000001519702DE80 (isActive = False)
    BaseList2D 'uvhook' at 0x000001519700EDC0 (isActive = False)
    BaseList2D 'ScatterPlacementHook' at 0x0000015197033CC0 (isActive = False)
    BaseList2D 'Tool System Hook' at 0x0000015197034AC0 (isActive = False)
    BaseList2D 'Simulation' at 0x000001519702C5C0 (isActive = False)
    BaseObject 'Simulation Scene' at 0x0000015197021980 (isActive = False)
    BaseList2D 'NE_SceneHook' at 0x000001519702CA80 (isActive = False)
    BaseList2D 'Take Hook' at 0x0000015197020400 (isActive = False)
    BaseTake 'Main' at 0x000001519703FD80 (isActive = False)
    BaseList2D 'Overrides' at 0x000001519703C200 (isActive = False)
    BaseList2D 'Others' at 0x0000015197035E40 (isActive = False)
    BaseList2D 'Layers' at 0x000001519701D2C0 (isActive = False)
    BaseList2D 'Materials' at 0x0000015197014BC0 (isActive = False)
    BaseList2D 'Shaders' at 0x000001519701B040 (isActive = False)
    BaseList2D 'Tags' at 0x000001519700A9C0 (isActive = False)
    BaseList2D 'Objects' at 0x000001519700D200 (isActive = False)
    BaseTake 'Take - Cube.Size.X' at 0x0000015197006B80 (isActive = True)
    BaseList2D 'Overrides' at 0x000001519703DC80 (isActive = False)
    BaseList2D 'Others' at 0x000001519701BD40 (isActive = False)
    BaseList2D 'Layers' at 0x000001519701AC40 (isActive = False)
    BaseList2D 'Materials' at 0x000001519701E700 (isActive = False)
    BaseList2D 'Shaders' at 0x00000151970088C0 (isActive = False)
    BaseList2D 'Tags' at 0x0000015197003780 (isActive = False)
    BaseList2D 'Objects' at 0x000001519701EA00 (isActive = False)
    BaseList2D 'CombineAc18_AutoCombine_SceneHook' at 0x0000015197014E40 (isActive = False)
    BaseList2D 'PLKHUD' at 0x000001519702B7C0 (isActive = False)
    BaseObject 'PKHOP' at 0x00000151970270C0 (isActive = False)
    BaseList2D 'RenderManager Hook' at 0x00000151970108C0 (isActive = False)
    BaseList2D 'Sound Scrubbing Hook' at 0x000001519702CFC0 (isActive = False)
    BaseList2D 'To Do' at 0x000001519701B5C0 (isActive = False)
    BaseList2D 'Animation' at 0x000001519700C3C0 (isActive = False)
    BaseList2D 'BaseSettings Hook' at 0x0000015197003D00 (isActive = False)
    BaseList2D 'PersistentHook' at 0x000001519703D4C0 (isActive = False)
    BaseList2D 'Scene Nodes' at 0x000001519702A680 (isActive = False)
    BaseList2D 'MoGraphSceneHook' at 0x000001519702C240 (isActive = False)
    BaseList2D '' at 0x000001519703E800 (isActive = False)
    BaseList2D 'StrNotFound' at 0x0000015197009800 (isActive = False)
    BaseList2D 'Sculpt Objects' at 0x0000015197006840 (isActive = False)
    BaseList2D 'HairHighlightHook' at 0x000001519700D940 (isActive = False)
    BaseList2D 'Mesh Check Hook' at 0x000001519702F6C0 (isActive = False)
    BaseList2D 'Modeling Objects Hook' at 0x0000015197041F40 (isActive = False)
    BaseList2D 'Snap Scenehook' at 0x0000015197032000 (isActive = False)
    BaseObject 'WorkPlane' at 0x000001519703F240 (isActive = False)
    BaseList2D 'Modeling Settings' at 0x00000151970189C0 (isActive = False)
    BaseList2D 'Doodle Hook' at 0x000001519701C380 (isActive = False)
    BaseList2D 'Stereoscopic' at 0x000001519703FD40 (isActive = False)
    BaseList2D 'ViewportExtHookHUD' at 0x000001519700B580 (isActive = False)
    BaseList2D 'ViewportExtHookhighlight' at 0x0000015197010400 (isActive = False)
    BaseList2D 'MeasureSceneHook' at 0x000001519701E740 (isActive = False)
    BaseList2D 'MeshObject Scene Hook' at 0x0000015197015E80 (isActive = False)
    BaseList2D 'Lod Hook' at 0x00000151970212C0 (isActive = False)
    BaseList2D 'Annotation Tag SceneHook' at 0x0000015197013900 (isActive = False)
    BaseList2D 'Sniper' at 0x0000015197025340 (isActive = False)
    BaseList2D 'Redshift SceneHook' at 0x000001519700BD00 (isActive = False)
    BaseList2D 'GvHook' at 0x000001519700F540 (isActive = False)
    BaseList2D 'Material Scene Hook' at 0x000001519703CDC0 (isActive = False)
    BaseList2D 'TargetDistancePicker' at 0x0000015197034240 (isActive = False)
    BaseList2D 'BodyPaint SceneHook' at 0x0000015197021000 (isActive = False)
    BaseList2D '' at 0x0000015197017E40 (isActive = False)
>>> 

And finally, the code:

"""Demonstrates traversing a classic API scene graph via branches. 
"""

import c4d
import typing

doc: c4d.documents.BaseDocument # The active document

# Toggle this for IterateGraph to print out some lines showing what it does.
IS_DEBUG: bool = False
# A table of (int, BaseList2D) tuples used by HasSymbolBase() below. Doing this is necessary,
# because classic API scene element branches express their content in terms of base types. So, we 
# might come along a branch with the ID Obase when iterating over a document. When we then want to
# find all Ocube instances in a scene, we must be able to infer that Ocube is an instance of Obase, 
# so that we know that we must branch into the Obase object branch to find Ocube instances. There
# is currently no other way to get this information except with BaseList2D.IsInstanceOf, i.e., we
# need an instance of a type symbol to test that. This table achieves that in a performant manner by
# only creating these dummy instances once.
G_TYPE_TESTERS_TABLE: dict[int: c4d.BaseList2D] = {}

def HasSymbolBase(t: int, other: int) -> bool:
    """Returns if the Cinema 4D type symbol #t is in an inheritance relation with the type symbol 
    #other.

    E.g., Ocube is an instance of Obase, Mmaterial is an instance of Mbase, but Tphong for example
    is not an instance of CTbase.
    """
    if not isinstance(t, int) or not isinstance(other, int):
        raise TypeError(f"Illegal argument types: {t, other}")
    
    # Try to get a previously allocated dummy node for the type symbol #t.
    dummyNode: typing.Union[c4d.BaseList2D, int, None] = G_TYPE_TESTERS_TABLE.get(t, None)
    # There is no node yet.
    if dummyNode is None:
        # Try to allocate one, this can fail, as the user could have passed a base symbol type.
        # E.g., one cannot allocate c4d.BaseList2D(c4d.Obase)

        # The symbol was a concrete type symbol, insert the dummy node under #t in the table.
        try:
            dummyNode = c4d.BaseList2D(t)
            G_TYPE_TESTERS_TABLE[t] = dummyNode
        # The type symbol was a base type, insert NOTOK under #t in the table.
        except BaseException:
            dummyNode = c4d.NOTOK
            G_TYPE_TESTERS_TABLE[t] = c4d.NOTOK

    # There can be no node instances for #t, #t must be a base type, compare #t with #other directly.
    if dummyNode == c4d.NOTOK:
        return t == other

    # Test if #t is an instance of #other
    return dummyNode.IsInstanceOf(other)


def IterateGraph(node: c4d.BaseList2D, 
                 types: typing.Optional[list[int]] = None,
                 inspectCaches: bool = False, 
                 root: typing.Optional[c4d.BaseList2D] = None) -> typing.Iterator[c4d.BaseList2D]:
    """_summary_

    Args:
        node: The starting node to inspect the contents for. Can be anything that is a BaseList2D,
            e.g., a document, an object, a material, a tag, a layer, etc.
        types (optional): The type of nodes which are in a relation with #node which should be 
            yielded. This will also respect inheritance, e.g., [c4d.Obase] will yield all objects,
            and [c4d.Ocube] will only yield cube objects. Will yield all node types when None. 
            Defaults to None.
        inspectCaches (optional): If the iteration should also branch into caches. Defaults to False.
        root: Private, do not override.

    Yields:
        Nodes which are in a relation with with #node and of a type or base type in #types.
    """
    # Stop the iterator when #node is None.
    if not isinstance(node, c4d.BaseList2D):
        return
    if IS_DEBUG:
        print (f"IterateGraph({node, types, root})")
    
    # When this is an first user call of this function and not a recursion we set #root to #node, 
    # so that we do not accidentally leak into other nodes. E.g, when we have this:
    #
    #  Node.0
    #  Node.1
    #   Node.1.0
    #   Node.1.1
    #  Node.2
    #  ...
    #
    # Then we usually do want for IterateGraph(Node.1, ...) to only look at Node.1, Node.1.0, and
    # Node.1.1, but not at anything after that, e.g., Node.2.
    if root is None:
        root = node
    
    # Start iterating over the nodes ...
    while isinstance(node, c4d.BaseList2D):
        # Yield the node itself if it does match the type criteria.
        if types is None or any(node.IsInstanceOf(t) for t in types):
            yield node

        # Yield nodes which are placed in branches below #node.
        #
        # Branches are basically just contextualized hierarchical relations, e.g, a document has
        # an object branch, where all objects are placed. We are iterating here over all these 
        # relations of a node. The content of a branch is not attached directly below a branch,
        # but under a GeListHead, a specialized variant of the hierarchy interface GeListNode of
        # the Cinema 4D classic API.
        if IS_DEBUG:
            print (f"\tBranches({node})")
        for branchDict in node.GetBranchInfo(c4d.GETBRANCHINFO_NONE):
            # The thing to which all nodes are attached which are in the #branchName relation with
            # #node. For a BaseDocument and its object branch, #geListHead.GetDown() would return 
            # the first object in that document.
            geListHead: c4d.GeListHead = branchDict.get("head", None)
            # A human readable description of the branch purpose, e.g., "Objects" for the object
            # branch of a document.
            branchName: str = branchDict.get("name", None)
            # The branch ID, this describes the base type of the nodes to find in this branch, for
            # the object branch of a document this would be c4d.Obase (5155).
            branchId: int = branchDict.get("id", None)
            # There are also flags in branch data, but I am ignoring them here.
  
            # This is malformed branching data, should not happen :)
            if None in (geListHead, branchId):
                continue
            if IS_DEBUG:
                print (f"\t\t{branchName = }, {branchId = }")
            
            # Get the first actual node in the branch, this can be None as Cinema often creates
            # empty branches in nodes for future access.
            firstNodeInBranch: typing.Optional[c4d.BaseList2D] = geListHead.GetDown()
            # Determine based on the branch type if we want to branch into this branch, i.e., the
            # user has passed Ocube and we must decide if we want to branch into Obase (yes, we 
            # do want to :)).
            shouldBranch: bool = (True 
                                  if types is None else 
                                  any(HasSymbolBase(t, branchId) for t in types))

            # Step over empty branches or branches which do not contain relevant nodes.
            if firstNodeInBranch is None or not shouldBranch:
                continue

            # Iterate over each node in the branch which is its own hierarchy with the root
            # #geListHead.
            for branchNode in IterateGraph(firstNodeInBranch, types, inspectCaches, root):
                yield branchNode
        
        # --- End of branching iteration.

        # Yield the content of BaseObject caches.
        if inspectCaches and isinstance(node, c4d.BaseObject):
            for cache in (node.GetCache(), node.GetDeformCache()):
                for cacheNode in IterateGraph(cache, types, inspectCaches, root):
                    yield cacheNode

        # Yield nodes which are in a direct hierarchical down relation with #node, e.g., the 
        # children of objects, layers, render data, etc.
        for descendant in IterateGraph(node.GetDown(), types, inspectCaches, root):
            yield descendant

        # We update node for the outer loop to the next sibling of #node. This a bit weird form
        # of handling direct hierarchies both with the outer loop, the loop above, and this
        # GetNext() call is necessary to implement this semi-iteratively, and thereby avoid full
        # recursion which could easily lead to stack overflows/Python's deep recursion mechanism
        # kicking in.
        #
        # We also only go to the 'next' thing when #node was not what the user considered to be
        # the root.
        node = node.GetNext() if node != root else None

def main() -> None:
    """
    """
    def printNode(node: c4d.BaseList2D) -> None:
        """Prints a node in a fashion convenient for this example.
        """
        # Print the class name of the node, e.g., BaseObject, its name, e.g., "Cube", the Python
        # __str__ style memory location of the node, and if the node is active, i.e., selected, or 
        # not.         
        memLoc: str = f"0x{hex(id(node)).upper()[2:].zfill(16)}"
        isActive: bool = node.GetBit(c4d.BIT_ACTIVE)
        if not IS_DEBUG:
            print (f"\t{node.__class__.__name__} '{node.GetName()}' at {memLoc} ({isActive = })")

    
    # A very simple call to the function, we just request all layers in the passed document. 
    print ("\nIterateGraph(doc, [c4d.Olayer])")
    for node in IterateGraph(doc, [c4d.Olayer]):
        printNode(node)

    # Iterate over all cube objects and any type of tag in the passed document.
    print ("\nIterateGraph(doc, [c4d.Ocube, c4d.Tbase])")
    for node in IterateGraph(doc, [c4d.Ocube, c4d.Tbase]):
        printNode(node)

    # We can also look at the partial graph of something, here we are looking for all phong tags and
    # descendant of the object named "Cube", in this case we are also traversing the caches of 
    # objects.
    print ("\nIterateGraph(cube, [c4d.Obase, c4d.Tphong], inspectCaches=True)")
    cube: typing.Optional[c4d.BaseObject] = doc.SearchObject("Cube")
    if isinstance(cube, c4d.BaseObject):
        for node in IterateGraph(cube, [c4d.Obase, c4d.Tphong], inspectCaches=True):
            printNode(node)

    # Where things get a bit tricky is when we want to look at branches which are inside branches.
    # One could implement #IterateGraph differently, so that it always iterate over everything, but
    # in the form I have implemented it here, the call below which asks for OverrideBase, i.e., 
    # take nodes will return nothing, because the takes are branches within branches, and we do not
    # go into any branches other than the take overrides. The same applies for the [c4d.Ocube, 
    # c4d.Tbase] example above, we only discover the tags because Ocube implies going into object
    # branches.
    print ("\nIterateGraph(doc, [c4d.OverrideBase])")
    for node in IterateGraph(doc, [c4d.OverrideBase]):
        printNode(node)

    # This will yield the overrides.
    print ("\nIterateGraph(doc, [c4d.Obase, c4d.OverrideBase])")
    for node in IterateGraph(doc, [c4d.Obase, c4d.OverrideBase]):
        printNode(node)

    # This would be the call for what you wanted, it will yield all objects, tags, materials, and
    # layers in #doc.
    print ("\nIterateGraph(doc, [c4d.Obase, c4d.Tbase, c4d.Mbase, c4d.Olayer])")
    for node in IterateGraph(doc, [c4d.Obase, c4d.Tbase, c4d.Mbase, c4d.Olayer]):
        printNode(node)
        # Parameters are not part of this data model, and once cannot retrieve the selection state
        # of a parameter in the Attribute Manager. But we can iterate over the description of a 
        # node.
        data: c4d.BaseContainer # The description container of the parameter
        descId: c4d.DescID # The id of the parameter.
        for i, (data, descId, _) in enumerate(node.GetDescription(c4d.DESCFLAGS_DESC_NONE)):
            # There are many things in a description container, I am only retrieving the name of the
            # parameter here, for details see the documentation of c4d.Description.
            name: str = data[c4d.DESC_NAME] or data[c4d.DESC_SHORT_NAME]
            # The value the parameter with the name #name currently has, one has however to be 
            # careful with such broad parameter access, not all data types are wrapped in Python.
            try:
                value: typing.Any = node[descId]
            except:
                value = "Not accessible"
            print (f"\t\tParameter '{name}'({descId = }): {value}")
            # Break out after the first three parameters, so that we do not saturate the console
            # too much :)
            if i >= 3:
                print ("\t\t...")
                break

    # We can also just iterate over everything in a node, here a document, by passing no type IDs.
    print ("\nIterateGraph(doc)")
    for node in IterateGraph(doc):
        printNode(node)

if __name__ == "__main__":
    main()

@ferdinand

Thanks for extreme detailed explains , but I think maybe had a bad description , so you go deeper .
In my views , your point at how to get all sence active nodes . ( It is a great explain and very useful to learn )

But my goal is get last select node ,for more specific minimal example , when a texture tag and a cube both selected , assign material maybe a problem expecially when scripts support them at same time , so the goal more like when cube is lastest select ,apply mat to cube , and vis versa , when texture tag is the lastest selected , then ignore the cube selection.

With your great codes , it more like returns a state more than a order .

And you said "There are however no equivalents for the selection order for materials, tags, etc." , Is that means I cannot approach it .

Like I have to set a more hard condition to decide what is consider first type ( It's a easy way , but not a user friendlly one , expecially to the C4D beginner , they don't want to remember so much priority conditions😂 , such as tag > object > layer or somethings)

@ferdinand Hello😊

Another question about this active things topic , with moving on , I check videopost in RenderData , but I find SetBit(c4d.BIT_ACTIVE) can't set a videopost selected .

For example :
I selected a Magic Looks , and I want to set another videopost like Redshift active , but it doesn't works , I test a BIT_VPDISABLED , it does worked . Do I use a wrong mask ?

And SetBit has some private pram like BIT_ACTIVE2 , how it works ?

Thanks 😁

Hello @dunhou,

Thank you for reaching out to us. I am struggling a bit with understanding what you want to do. When we look and the following Render Settings dialog, we can see that there are two kinds of things we could consider 'active'.

c3ee6bc7-d332-42a8-a41f-62bbaa56dfaa-image.png

The Magic Bullet Looks video post is active in the sense that it has been enabled in the render settings to contribute to a rendering. This is indicated by the little check box next to it and can be set programmatically with BIT_VPDISABLED.

Then there is the Denoiser videopost which is active in the sense as it is selected in the window. This state is not exposed in the API and you can neither read nor write it.

Cheers,
Ferdinand

@ferdinand

Unfortunally , my purpore is the 2rd : set the state of Denoise . I think I should forgot this idea😂

eg : find a certain tab in redshift like the color spcae and show it first always when open rendersetting
d36b6865-dac1-4ab9-a1be-3f42be3407b9-image.png

Thanks ~

Hey @dunhou,

you cannot do that. Both things, setting the active video post item in the GeListView on the left and setting the active tab in the DescriptionCustomGui on the right, would require access to the dialog implementation. The former type is also only available in C++.

In general, the Cinema 4D APIs provide no or only extremely limited access to the manger implementations of Cinema 4D. This is very much intentional, as we do not want third parties to write plugins which for example forcibly set the selected video post node in the Render Settings and then also open a specific tab within that node. As this could easily confuse users or lead them to the conclusion that something is broken with the Render Settings dialog, not knowing the behavior is caused by a plugin they have installed.

When you want to just display/modify data of a specific video post node, you should write a GeDialog with a DescriptionCustomGui in it to display the node. I once showed here how this would work for the user preferences, find also a modernized example for your use case at the end. The caveat is that in Python DescriptionCustomGui::SetActiveTabPage has not been wrapped, which will make it impossible [1] for you to set the active tab. You would have to use C++ for that.

Cheers,
Ferdinand

[1] Technically, there might be ways to circumvent these limitations, when getting creative with descriptions. But this would be very much a hack and therefore out of scope of support. I cannot help you with that. Also, and this might be highly subjective, just using C++ will be much easier than trying to hack the description of the redshift node in Python.

The result:
632bc4b9-43c6-4e62-977a-c8b469d43e89-image.png
The code:

"""Provides a simple example for displaying a video post node with a CUSTOMGUI_DESCRIPTION in a dialog.
"""

import c4d
import typing

doc: c4d.documents.BaseDocument # The active document.
dialog: c4d.gui.GeDialog # Global RedshiftDialog instance used by #main().


class RedshiftDialog(c4d.gui.GeDialog):
    """Implements a dialog which displays a node in a CUSTOMGUI_DESCRIPTION.

    This specific implementation also retrieves the Redshift video post node of a passed document
    and sets it as the to be displayed node. When there is no such node in the passed document, the
    type will raise an error.
    """
    ID_DESCRIPTION_GUI: int = 1000

    def __init__(self, doc: c4d.documents.BaseDocument, 
                 onCloseCallback: typing.Optional[callable] = None) -> None:
        """Inits the dialog instance.
        """
        self._onCloseCallback: typing.Optional[callable[object]] = onCloseCallback

        self._doc: c4d.documents.BaseDocument = doc
        self._descriptionGui: typing.Optional[c4d.gui.DescriptionCustomGui] = None
        self._redshiftVideoPostNode: c4d.documents.BaseVideoPost = self._getRedshiftVideoPost()

    def _getRedshiftVideoPost(self) -> c4d.documents.BaseVideoPost:
        """Attempts to retrieve the Redshift video post node from #self._doc.
        """
        renderData: c4d.documents.RenderData = self._doc.GetActiveRenderData()
        videoPost: c4d.documents.BaseVideoPost = renderData.GetFirstVideoPost()

        while videoPost:
            # This is 2023.0 code, in prior releases you must replace the symbol c4d.VPrsrenderer 
            # with the integer 1036219.
            if videoPost.GetType() == c4d.VPrsrenderer:
                break
            videoPost = videoPost.GetNext()

        # When #videoPost is #None at this point, it means there is no Redshift video post in the
        # active render settings of #doc. You could of course also add it yourself here, but I did
        # not do that.
        if not isinstance(videoPost, c4d.documents.BaseVideoPost):
            raise RuntimeError("Could not find Redshift renderer in document.")
        
        return videoPost

    def CreateLayout(self) -> None:
        """Adds the description custom GUI to the dialog which is used to display the Redshift video
        post node.
        """
        self.GroupBorderSpace(10, 10, 10, 10)

        bc: c4d.BaseContainer = c4d.BaseContainer()
        bc[c4d.DESCRIPTION_ALLOWFOLDING] = True
        bc[c4d.DESCRIPTION_SHOWTITLE] = False
        bc[c4d.DESCRIPTION_NOUNDO] = False
        bc[c4d.DESCRIPTION_OBJECTSNOTINDOC] = True
        bc[c4d.DESCRIPTION_FORCEGETOBJECT] = True
        bc[c4d.DESCRIPTION_MODALMODE] = 1
        bc[c4d.DESCRIPTION_LEFTMATEDITREGION] = False
        bc[c4d.DESCRIPTION_SCALE_ALL_ELEMENTS] = False
        bc[c4d.DESCRIPTION_NO_SHOW_SUBCHANNELS] = False
        bc[c4d.DESCRIPTION_OPEN_ALL_GROUPS] = False
        bc[c4d.DESCRIPTION_NO_TAKE_OVERRIDES] = False
        bc[c4d.DESCRIPTION_SINGLEDESC_MODE] = False
        bc[c4d.DESCRIPTION_MANAGER_TYPE] = True
        bc[c4d.DESCRIPTION_HIDE_EMPTY_GROUPS] = True

        self._descriptionGui = self.AddCustomGui(
            RedshiftDialog.ID_DESCRIPTION_GUI, c4d.CUSTOMGUI_DESCRIPTION, "",
            c4d.BFH_SCALEFIT | c4d.BFV_SCALEFIT, 600, 300, bc)
        if not isinstance(self._descriptionGui, c4d.gui.DescriptionCustomGui):
            return MemoryError("Could not allocate CUSTOMGUI_DESCRIPTION.")

        self._descriptionGui.SetObject(self._redshiftVideoPostNode)
        return True

    def AskClose(self) -> bool:
        """Call our callback when the dialog is about to be closed.

        Not really needed in this case and only a fragment of the old example.
        """
        if self._onCloseCallback:
            self._onCloseCallback(self)
        return False

def on_close(host: object) -> None:
    """Called by the dialog instance #MY_DIALOG when it is closing.
    """
    print (f"Whee, I got called by a '{host.__class__.__name__}' instance.")


def main() -> None:
    """
    """
    # This is the global dialog instance hack to open and maintain an async dialog in a Script 
    # Manager script. It will result in a dangling dialog reference when the script has closed,
    # please do not use this hack in a production environment as it can lead to crashes and 
    # therefore data loss.
    global MY_DIALOG
    MY_DIALOG = RedshiftDialog(doc, on_close)
    MY_DIALOG.Open(c4d.DLG_TYPE_ASYNC)


if __name__ == "__main__":
    main()

@ferdinand

Thanks for the detailed explain😳

I think DescriptionCustomGui is the best way to solve this for now .

With learning furthur , maybe I will try a C++ version , but for me it's not time .

Anyway , It is helpful for this techniclly explain and the example🤝