SOLVED Undo for InsertUnder() is not right. Why?

Undo for InsertUnder() is not right. Why?
The code below:
484e1741-a95f-4822-b48a-fba442a0041c-image.png
The operations below:
dfc966ba-afb0-4042-9f8b-7ed824d28062-image.png
After executing the script:
90c8752e-afd3-47fe-9efe-35676aeb084c-image.png
After several undo operations:
f14cf3ca-9d2a-4310-bb91-2762400c6149-image.png

The first question is that the undo result is not right.
The second question is that the undo times are not right.
There should be one time for undo to get the correct undo result if there is no problem.

Hello @lingza,

Thank you for reaching out to us. The likely reason why your script is not working is the position of your AddUndo call. As the Python documentation states,

always has to be called before a change is made. In the case of the creation of a new object the call is done afterwards,

There is also a slight ambiguity with how to treat hierarchy modifications: as a UNDOTYPE_CHANGE event or as an UNDOTYPE_DELETEOBJ and UNDOTYPE_NEWOBJ event. You can do both, but as hinted at in the UNDOTYPE_CHANGE description, hierarchy modifications also count as changes. Find a script example below which works fine for me, i.e., undo does revert to the state before running the script.

Cheers,
Ferdinand

Scene state before the script ran (and also the state after an undo):
before.png
Scene state after the script ran:
after.png

The script:

"""Provides an example for creating undo items for hierarchy modifications.
"""

import c4d

doc: c4d.documents.BaseDocument  # The active document

def main() -> None:
    """
    """
    # Get the active selection as you did.
    selection: list[c4d.BaseObject] = doc.GetActiveObjects(
        c4d.GETACTIVEOBJECTFLAGS_CHILDREN | c4d.GETACTIVEOBJECTFLAGS_SELECTIONORDER)
    if len(selection) < 2:
        return
    
    # We declare the first item in the list our root to which we are going to parent things.
    root: c4d.BaseObject = selection.pop(0)

    # Start an undo stack.
    doc.StartUndo()
    for item in selection:
        # Here you got things mixed up a bit. An undo item must be added before the action is 
        # carried out, e.g., UNDO_CHANGE, and only after when a new item is being added, i.e., 
        # UNDOTYPE_NEWOBJ. While the situation might be ambiguous here - is moving an item adding
        # an item or not, your symbol UNDO_CHANGE did not line up with place where you called 
        # AddUndo.

        # I decided to go the easy route and treat this just as a change event and not as a removal
        # and new object event.
        doc.AddUndo(c4d.UNDO_CHANGE, item)
        # A node can only be attached to one thing. In the Python layer, GeListNode.Remove() is
        # called automatically when one calls one of the insertion methods, in C++ not. So, this line 
        # is only for verbosity.
        item.Remove()
        # Insert the item under the root.
        item.InsertUnderLast(root)

    # Close the undo stack and push an update event to Cinema 4D.
    doc.EndUndo()
    c4d.EventAdd()

if __name__ == '__main__':
    main()

Thanks! Sorry, I am so careless that I ignore the message "Needs to be called before the change.".

UNDOTYPE_CHANGE:

Any change to an object, including hierarchy modifications; modification in positioning (object has been moved from A to B), substructures etc. (Needs to be called before the change.)

Thanks! Sorry, I am so careless that I ignore the message "Needs to be called before the change.".

No worries, you are not the first one who trips over Undo call order, including myself at some point 😉