GetLayerObjectRoot and GeListHead



  • Hello;

    regarding GeListHead: The documentation tells us that we find the first and last list element by GetFirst() and GetLast(). Now, in BaseDocument, we have the function GetLayerObjectRoot() which supposedly returns a GeListNode, and where we find the following sample code:

    def GetFirstLayer(doc):
        # Returns the first layer, if available, otherwise None
    
        return doc.GetLayerObjectRoot().GetDown()
    
    1. GetLayerObjectRoot is actually returning a GeListHead, not a Node. That's what the C++ API is doing, at least. (Just checked, yes, the type in Python is GeListHead too.) So, the documentation on that function is wrong.
    2. The return value here can nevertheless give us the list by GetDown(). That suggests that GeListHead stores its First list pointer in the same attribute as the Down reference of the superclass. I checked that, and in fact GetFirst() and GetDown() point to the same objects for the BaseObject list in the document and the Shader list in a material. -- OTOH the Pred, Next, and Up references in the GeListHead seem to be None. I am now wondering whether C4D has any construct where GeListHeads are linked with each other in lists or trees, or whether the parent class's inherited functions are mostly meaningless and will always return None. -- As GeListHead stores references to the Last and Parent objects, these seem to be separate references internally, in Python at least. Question: Is there a reason that GetDown() is used here, or is this just a relic from some earlier API version where GetLayerObjectRoot returned a GeListNode erroneously? Searching this forum turns up some very old posts where GetLayerObjectRoot seems to be used differently.
    3. The sample code is annotated with the phrase "Warning: The hierarchy method GeListNode.GetDown() can only be called on the returned object." I have absolutely no idea what that means. Layers are organizes hierarchically, so of course I can call GetDown() on any other layer. Naturally the call may return None. Given the fact that WARNINGS are not found that often in the API docs, I assume that this should be taken seriously - but what is it actually warning us of?

    Thanks for any insights!



  • Hi,

    no offense, but I find your posting rather hard to read, so I might have missed some points.

    1. The C++ docs have the correct signature. The Python docs telling you the return value being a GelIstNode are inacurate, but not really wrong, since a GeListHead is a GeListNode.

    2. Your observation with GetFirst for a GeListHead is quite interesting, but I do not understand why you would expect not to use GetDown on a GeListHead, since they are the specialized root-node version of Cinema's nodes?

    3. Unless I missed it, you did not really told us where you did find this. But it sounds like the author wanted so say something like the following (instead of your quoted part), hinting at the fact that GeListHeads are root-nodes, that only expand downwards.

      Warning: Only the hierarchy method GeListNode.GetDown() can be called on the returned object.

    Cheers,
    zipit



  • I am talking primarily about the description of GetLayerObjectRoot, which can be found here:

    https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d.documents/BaseDocument/index.html?highlight=layer#BaseDocument.GetLayerObjectRoot

    This entry in the API documentation contains both the code snippet that I copied in my first post, as well as the warning. Call it "inaccurate" as you wish (I call it plain wrong); but I would not even mind if the code snipped would not explicitly use GetDown() instead, and there wouldn't be a warning.

    I see how you interpret the warning, and it kind of makes sense... even if that is something completely different from what the original text says, and it's no problem at all to call the other hierarchy functions, they just all return None.

    The whole documentation on GeListHead/Node and the tree structure is hard to understand and in parts contradictive.

    Here's what I found and verified so far:

    20200418_2000_01__DiagramListHead.jpg

    This is the functional connection between GeListHead and some GeListNodes, here a BaseDocument and some BaseObjects. (Other functions like GetFirstObject and GetDocument are in a different diagram.)

    • While the nodes are called GeListHead and GeListNode, they are actually Tree nodes (four way connections).
    • GeListHead exists only as anchor for a list or tree, and is a single node that anchors the full tree - lower levels of the tree do not have another GeListHead per level.
    • The GeListHeads of the basic C4D structures (e.g. for objects, materials, layers) only employ the Down links to reference the actual lists and trees. The Up, Pred, and Next links are None. GeListHeads for an object (like BaseDocument) are not linked with each other.
    • The GeListHead link for GetFirst() is the same as the Down link from its superclass GeListNode. The link for GetParent() is introduced by GeListHead, it does not exist in GeListNode. The functions GeListNode.GetDownLast() and GeListHead.GetLast() return the same node, but it is not specified anywhere whether there is a permanent pointer in either class to speed up the access to the last node, or whether this node is calculated on the fly.
    • Most sub-tree structures are accessed by a specific function that returns the first node: GetFirstObject, GetFirstMaterial, GetFirstTag, GetFirstShader.
    • The notable exception is GetLayerObjectRoot, returning a GeListHead, which is why I kind of obsess with that one.

    I reeeeeally wish I could just look into the original code.



  • I would be content with my current understanding, but it's still funny:

    The GeListHead manual in the C++ API documentation explicitly says
    GeListHead itself is based on GeListNode. So instances of GeListHead can be organized in a list or tree:
    GeListHead::InsertBefore(): Inserts the element before the given GeListNode in the list.
    GeListHead::InsertAfter(): Inserts the element after the given GeListNode in the list.
    GeListHead::InsertUnder(): Inserts the element under the given GeListNode.
    GeListHead::InsertUnderLast(): Inserts the element as the last child element under the given GeListNode.

    The very same functions are then defined as private in the GeListHead Class Reference. So... the manual suggests that you link GeListHeads, but the GeListHead docs say you'd better not, and the C4D data structures don't use it.

    Moreover:

    To link a node in a tree, you need to use 4 pointers, up, down, pred, next, which the GeListNode provides. If you want a payload for a tree node, you need a separate pointer to the content.
    In the GeListHead, the whole tree (or list) is the payload. So, GetFirst() should use a new pointer, introduced in this class, to access the sub-structure. It should not re-use the Down pointer, because that would clash with the GeListHead tree structure itself, should you actually want to link GeListHeads into a tree. Which the GeListHead manual explicitly allows!!

    On the other paw: You may argue that the GeListHead should not be linked into a structure, and the four tree pointers up, down, pred, next are unused. Yet, the access function GetParent() is not returning the same as GetUp(). Why is the up pointer not being reused, too?

    I guess that may all be minor inconsistencies that count as technical debt and cannot be straightened because it would break the API. But maybe I'm wrong, and I have a serious issue in my understanding. In that case, I would like to know before I teach others wrong stuff.



  • Hi,

    the major reason for the existence of GeListHead - aside from the advantages of having a dedicated root node type - is the probably the fact that Cinema's scene graph is mono-hierarchical. The GeListHead type stitches the partial scene graphs together, so that you can have for example BaseTag and BaseObject children attached to a node without mixing them up. GetParent is the realization of that partial graph link, while GetUp on a GeListHead would traverse that local graph, although logic would dictate that there is nothing above a root node. That is also so reason why you won't find a GeListHead in the midst of one of the partial graphs, for example the BaseObject graph of a document.

    I still do not really get the point on all the pointer stuff. While it is certainly possible that GeListNode is realized with four pointers, it could also be just two plus some computational effort on each call. Also the term payload is in my option a rather misleading term here, since the data attachment is either realized by the equally confusingly named BaseList2D or just child nodes, but I might be misunderstanding you here.

    Cheers,
    zipit



  • Hello;

    I can imagine why GeListHead exists - it does make sense to have an anchor with control attributes for a referential structure like lists / trees.
    Where the API is losing me is: why is GeListHead derived from GeListNode? As far as I have verified, three of the pointers of GeListNode are not used, and the fourth is semantically re-interpreted so it's not even possible to use GeListHead in the same way as a GeListNode.

    Even that would be a negligible (technical debt; revised decisions; breaking the API etc pp) but the documentation (manual) for the GeListHead explicitly tells me that I can use the insertion functions from GeListNode, which actually I can't, if one of these pointers is no longer doing what GeListNode expects from it. Also, GeListHead is not actually used that way.

    I'm maybe overthinking it, and there are no actual errors coming from it, but its a major weirdness.

    To answer your musings from the last post:

    It does not matter whether GeListNode internally uses four or two pointers - I cannot see the implementation for lack of the original source code. (I'd bet my money on four, as the alternative would add so much calculation time that it becomes unfeasible.) But that's not the crux. What matters is the reinterpretation of the down pointer as pointer to the attached list or tree structure, which is not the same semantics as attaching another part of the tree. As you say yourself, the tree consists of the same type of elements, which is a concept that gets broken there. The node tree that is attached to the GeListHead is not part of the (hypothetical) GeListHead tree (which is why I use the word "payload" - it's content, not the same structure). If you do that, you cannot build a tree from GeListHead anymore while at the same time adding a payload (a tree of mono-class objects other than GeListHead) because the down pointer is already "used up".

    If I had implemented this according to the functionality I actually see in the resulting structures, I'd not used GeListNode as base class, and I would have introduced the Parent, First, and Last pointers all equally to the new class. Then, GeListHead would not be able to form a tree by itself, and all the questions would dissolve. (Granted, I am not qualified to refactor this, as I don't know all the other conditions and necessities that are behind the parent class decision, but from where I stand this would look cleaner.)

    I'm dropping the matter for now and assume that GeListHead is actually not used as tree node by itself, and the documentation is way off there. If I find evidence of the contrary, I'll re-examine the question.



  • Hi Cayrin, thanks for the very long and detailed exposition.

    Although I appreciate your effort in investigating and presenting the topic, I would really recommend, at the end of your dissertations,to pin point your questions for the sake of better readibiltiy and support.

    GetLayerObjectRoot is actually returning a GeListHead, not a Node. That's what the C++ API is doing, at least. (Just checked, yes, the type in Python is GeListHead too.) So, the documentation on that function is wrong.

    Yes, you're right. We're going to rectify it.

    Question: Is there a reason that GetDown() is used here, or is this just a relic from some earlier API version where GetLayerObjectRoot returned a GeListNode erroneously?

    Being that snippet part of the GeListHead documentation I agree that it looks improper and GeListHead::GetFirst() should be used instead. Nonetheless I confirm that GeListHead::GetFirst() and GeListNode::GetDown() return the same GeListNode pointer.

    The sample code is annotated with the phrase "Warning: The hierarchy method GeListNode.GetDown() can only be called on the returned object."
    I would read it as @zipit has interpreted although I agree it's missleading. Nonetheless with the replacement of GetDown with GetFirst the warning will be gone.

    GeListHead itself is based on GeListNode. So instances of GeListHead can be organized in a list or tree:

    GeListHead::InsertBefore(): Inserts the element before the given GeListNode in the list.
    GeListHead::InsertAfter(): Inserts the element after the given GeListNode in the list.
    GeListHead::InsertUnder(): Inserts the element under the given GeListNode.
    GeListHead::InsertUnderLast(): Inserts the element as the last child element under the given GeListNode.

    Starting with the consideration that it actually should be part of the "Insertion" section rather than the "Navigation" one, looking at the code, I confirm that GeListHead was not designed with the idea of having it organized in trees or lists but actually to behave as a Head. This suspect is "confirmed" by the fact that those insertion methods are marked private on purpose. We'll rectify the corresponding manual to fix this.

    Finally, about the scope of GeListHead, I agree with @zipit saying in GetLayerObjectRoot and GeListHead:

    The GeListHead type stitches the partial scene graphs together, so that you can have for example BaseTag and BaseObject children attached to a node without mixing them up. GetParent is the realization of that partial graph link, while GetUp on a GeListHead would traverse that local graph, although logic would dictate that there is nothing above a root node.

    If there's anything I've answered back, feel free to get back.

    Best, Riccardo



  • Thanks for the confirmation. I shall treat the extra link directions as nonexistent then ;-)