Universal "Undo" to Rule them All?



  • Hi,

    So far. I know is there no built-in undo.
    So, it must be something like this

    doc.StartUndo()
    
    # Do stuff here
    
    doc.EndUndo()
    

    For the AddUndo(), it would be just optional if you want to really store some undo steps (for which I have no intention).

    Anyway, despite following that, it wouldn't work as expected.
    You can see the result of undoing in this illustration video

    The script is a bit long for a post, so here is a dropbox link instead.

    Is there a way to fix my undo problem in the script? A universal undo maybe? *wink *wink



  • Nah. You can't use Start/EndUndo without defining what to undo through an AddUndo. While there are a few commands that have the whole Undo sequence built in, you will normally want to define your AddUndos yourself so you are in control of the process.

    Imagine it like that: Start/EndUndo define just a "container" which is added to the undo list. When the user presses "Undo", this container will try to restore the previous state by applying all its content (multiple changes) in reverse sequence. If you don't put anything into this container by calling AddUndo, well, the Undo process does not have any information on what to change back. After all, the Undo need to know what the previous state even was.

    For example, if you want to delete an object, you will AddUndo(UNDOTYPE_DELETE, obj) before the deletion happens - because after the deletion, the object is gone. The AddUndo will keep a copy of the deleted object so it can later restore it. Without an AddUndo, there is no such copy and the Undo doesn't know what you deleted and what to restore.

    Now, why is there such a container structure? After all, Maxon could just build an undo into every command?

    Yes they could, but this would create an enormous overhead that may not be needed in all cases (for temporary objects, e.g.). Not defining an undo step explicitly through Start/EndUndo would also mean that the user would have to press Undo for every such substep, instead of pressing it once to revert the results of the whole script. If the script does three hundred object changes, the user would need to press Undo 300 times. Not very practical.

    Could Maxon at least put an AddUndo into every command then, and leave only the Start/EndUndo markers in place? Yes they could, but still, you wouldn't want to burden the system with tons of Undo objects that may never be needed. So, you have to decide yourself what objects to make "undoable" and where to use just straightforward code.

    In fact, undoing is one of the, eh, more interesting topics in software. If you use an undo schema as in C4D, you still have to look out for exceptions and unwanted returns, so you don't accidentally skip an EndUndo. You also may need to trigger an undo in error cases to revert to the original structure before exiting (controlled fail). And naturally you have to think about what AddUndos to actually place.

    Darn, I could have sworn there was a whole chapter on Undoing in the API manual but now I can't find it. >:-(



  • Thanks for the detailed response @Cairyn

    RE: If the script does three hundred object changes, the user would need to press Undo 300 times. Not very practical.
    I agree, not very practical but so is identifying every possible AddUndo.

    I'm sorry but I just can't help it comparing with Maya's python scripting. Where I execute a script, hit Undo, and intuitively, it undos everything.

    RE: So, you have to decide yourself what objects to make "undoable" and where to use just straightforward code.
    It would seem like I would spend a hefty amount of time what objects to be undoable on complicated scripts, if this is the case.
    The thing is I don't really care about what objects per se is undoable or not. I just want the whole script to be undoable.

    RE: AddUndo()
    For the sake of the argument, supposing I managed to AddUndo() in the scripts. Supposing 5. This would mean I have to press Ctrl+Z 5 times right?

    I just want it to function like maybe in a plug-in, where a command contains several functions but when I hit undo, it undoes everything.

    I just want life to be simplier :(



  • @bentraje I am not using Maya so I can't say anything about that. As a programmer though, I know that there ain't nothing like no free launch... uh, lunch. If Maya is able to undo/redo automatically, then the Python API needs to have that functionality built in (automatic call of something like StartUndo at the beginning, EndUndo at the end, and every API call has an AddUndo integrated. That is possible, yes. You have to pay a price for it though, both in development time (of the API programmers) and in execution time. Maybe that is a good tradeoff; I can't judge that. It's certainly easier for the Python programmer

    Personally, I admit that I am a lazy pig and often don't integrate any undo in my scripts. But it is not that difficult as you make it sound right now. You would only have to add an AddUndo at points where you really make a change to the object tree (or related internal data). The actual calculations are unaffected. If you have a really complicated script, it would be best if you create it in layers, with the changes made to the object tree being at as high level in the call hierarchy as possible. This allows for safe exits and controlled error situations. (But we're far off topic now.)

    No, you would not need to press Ctrl-Z five times. Look at the following:

    import c4d
    
    def main():
        doc.StartUndo();
        obj = doc.GetFirstObject();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        doc.EndUndo();
        c4d.EventAdd();
    
    if __name__=='__main__':
        main()
    

    (warning: If you execute this script you MUST have 5 objects at top level, or the thing will crash.)
    Try it, it will rename the objects; one Ctrl-Z is enough to revert all changes.
    Now compare this with the following:

    import c4d
    
    def main():
        doc.StartUndo();
        obj = doc.GetFirstObject();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        doc.EndUndo();
        doc.StartUndo();
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        doc.EndUndo();
        doc.StartUndo();
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        doc.EndUndo();
        doc.StartUndo();
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        doc.EndUndo();
        doc.StartUndo();
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        doc.EndUndo();
        c4d.EventAdd();
    
    if __name__=='__main__':
        main()
    

    Here, each AddUndo is encapsulated into a Start/EndUndo block. Now use the script and undo - you will find that each undo only reverts one rename!
    So, the Start/EndUndo blocks are the ones that control the "container" that is reverted by Ctrl-Z (as mentioned in my previous post). If you want to revert everything at once, just encapsule the whole execution of the script in the main() function with a Start/EndUndo block. It is not necessary to repeat this block with every method you write.

    Now have a look at the following:

    import c4d
    
    def main():
        doc.StartUndo();
        obj = doc.GetFirstObject();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        doc.EndUndo();
        obj = obj.GetNext();
        obj.SetName("Renamed!");
        obj = obj.GetNext();
        obj.SetName("Renamed!");
        c4d.EventAdd();
    
    if __name__=='__main__':
        main()
    

    Execute and undo... Ooops! Only three renames were reverted! As obvious from the script, two renames are outside of the Start/EndUndo block and have no AddUndo either, so Ctrl-Z will not affect these.

    I leave further exploration to you, but I fear you will not get a super simple undo automation as in Maya.



  • @Cairyn

    Thanks for the response.

    RE: the C4D vs Maya code architecture
    I'll put that to rest since there's really no way around it, but thanks for indulging me.

    RE: No, you would not need to press Ctrl-Z five times. Look at the following:
    Thanks for the detailed overview but correct me if I'm wrong but the UNDOTYPE_CHANGE_SMALL parameter only applies to small changes.

    In the script, I'm adding a new object such as duplicate a joint hierarchy and creating constraints.

    As such, I have to hit undo several times since I have to use UNDOTYPE_NEW paramater even if I added it in a single StartUndo and End Undo container.

    Or maybe I'm using it wrong?



  • @bentraje Nope. Only the Start/EndUndo containers determine how often you need to hit Ctrl-Z. Not the AddUndos that you issue in between, or the type of these AddUndos. Look at the following:

    import c4d
    
    def main():
        doc.StartUndo()
        cube = c4d.BaseObject(c4d.Ocube)
        doc.InsertObject(cube, None, None)
        doc.AddUndo(c4d.UNDOTYPE_NEW, cube)
        sphere = c4d.BaseObject(c4d.Osphere)
        doc.InsertObject(sphere, cube, None)
        doc.AddUndo(c4d.UNDOTYPE_NEW, sphere)
        plat = c4d.BaseObject(c4d.Oplatonic)
        doc.InsertObject(plat, None, sphere)
        doc.AddUndo(c4d.UNDOTYPE_NEW, plat)
        doc.EndUndo()
        c4d.EventAdd()
    
    if __name__=='__main__':
        main()
    

    This code creates a hierarchy of three objects, each with their own AddUndo but all in the same Start/EndUndo block. That means all three objects will be removed at one single Ctrl-Z. Try it.

    Also, if you cut out the generation of the objects and put it into a method, this will change nothing, it will just make the Start/EndUndo bracket in your code more visible:

    import c4d
    
    def create():
        cube = c4d.BaseObject(c4d.Ocube)
        doc.InsertObject(cube, None, None)
        doc.AddUndo(c4d.UNDOTYPE_NEW, cube)
        sphere = c4d.BaseObject(c4d.Osphere)
        doc.InsertObject(sphere, cube, None)
        doc.AddUndo(c4d.UNDOTYPE_NEW, sphere)
        plat = c4d.BaseObject(c4d.Oplatonic)
        doc.InsertObject(plat, None, sphere)
        doc.AddUndo(c4d.UNDOTYPE_NEW, plat)
            
    def main():
        doc.StartUndo()
        create()
        doc.EndUndo()
        c4d.EventAdd()
    
    if __name__=='__main__':
        main()
    

    So you can just do that and put the Start/EndUndo bracket in the top call, and then work your way through your code by only using the proper AddUndos.

    Now you will ask, why the different undo types? This helps C4D to determine the extent of change that will be done (mostly, AddUndo comes before the change...), and therefore minimize the amount of data stored in the undo list. For example, if you only change a flag on a polygon object with 100 000 polys, then it would be an excessive waste to store the full object in the undo list. It's quite sufficient to store the BaseContainer, or even a single value from that container, to revert and redo the change. (Keep in mind that you don't just want to undo but at times also to redo an undone operation.)

    If you know what the AddUndo really stores for future Undos and Redos, you can save yourself some AddUndos. Try the following code:

    import c4d
    
    def create():
        cube = c4d.BaseObject(c4d.Ocube)
        doc.InsertObject(cube, None, None)
        doc.AddUndo(c4d.UNDOTYPE_NEW, cube)
        sphere = c4d.BaseObject(c4d.Osphere)
        doc.InsertObject(sphere, cube, None)
        plat = c4d.BaseObject(c4d.Oplatonic)
        doc.InsertObject(plat, None, sphere)
            
    def main():
        doc.StartUndo()
        create()
        doc.EndUndo()
        c4d.EventAdd()
    
    if __name__=='__main__':
        main()
    

    There is only one AddUndo while three objects are generated. But since the two other new objects are linked as children of the first new object, the whole substructure will behave as one - if you undo, you will still see all three objects disappear, and - more important! - if you redo, all three will appear again!

    Now, I have no access to the C4D undo code, but obviously the undo cuts the new object from the hierarchy including everything that is attached to it, and keeps the structure intact for a later redo.

    If you had linked one of the child objects somewhere else, you would need an additional AddUndo for it, naturally. E.g. if you create the Platonic as top level object, it would no longer be affected by the Undo of a parent object.



  • @bentraje Meh, I just noticed my first code is not a good example because of what I later wrote. Since the top object of the created hierarchy is already determining the fate of the subobjects, the additional AddUndos have no true function. That does not make it wrong but it does not properly prove what I claim ;-)

    Try the following:

    import c4d
    
    def create():
        cube = c4d.BaseObject(c4d.Ocube)
        doc.InsertObject(cube, None, None)
        doc.AddUndo(c4d.UNDOTYPE_NEW, cube)
        sphere = c4d.BaseObject(c4d.Osphere)
        doc.InsertObject(sphere, None, None)
        doc.AddUndo(c4d.UNDOTYPE_NEW, sphere)
        plat = c4d.BaseObject(c4d.Oplatonic)
        doc.InsertObject(plat, None, None)
        doc.AddUndo(c4d.UNDOTYPE_NEW, plat)
            
    def main():
        doc.StartUndo()
        create()
        doc.EndUndo()
        c4d.EventAdd()
    
    if __name__=='__main__':
        main()
    

    Here, the three objects are created independently on top level, yet still all disappear with one CTRL-Z.

    (And again, this is true for all types of AddUndo.)



  • @Cairyn

    As usual, thanks for the detailed response. Helps a lot. (Do you have a blog where you post this kind of stuff? Would love to read it for random nuggets)

    Your explanation works but I got one problem: I think its the Callcommand.

    Just to recap my script procedures
    (1) Duplicate Chain
    (2) Rename the chain using the naming tool (which is uses the CallCommand
    (3) Add PSR Constraints to the base chain for IK and FK

    Now, with the complete set-up above, I need to undo three times.
    If I remove the (2), I only undo single time.

    So I think CallCommand is the problem, which I believe in my memory has a separate undo behavior (or something like that).

    My problem is what UNDO_TYPE I should use and what part of the code.

    The code is in the link above posted but for reference here is the snippet:

        # Rename the chain
        naming_toolID = 1019952
        c4d.CallCommand(naming_toolID)
    
        namingTool = c4d.plugins.FindPlugin(naming_toolID, c4d.PLUGINTYPE_TOOL)
        namingTool[c4d.ID_CA_JOINT_NAMING_REPLACE] = old_name
        namingTool[c4d.ID_CA_JOINT_NAMING_REPLACE_WITH] = new_name
        c4d.CallButton(namingTool, c4d.ID_CA_JOINT_NAMING_REPLACE_APPLY)
    
    

    Thank you for looking at the problem. Appreciate it a lot!



  • The thing with CallCommand() is, that it calls Cinema 4D functions. The CallCommand() does not know, what these functions do, therefore it does not "know" where to put Undos. So, the functions itself implement the Undos obviously.



  • @mp5gosu

    Thanks for the response.
    Is it safe to say that with the CallCommand() function, I cannot attain one Undo for the whole script? Or do you have suggestion in place other than using CallCommand() function?

    The reason is looking forward now, I'd probably use at least 10 CallCommand in a final version of the script that I am working at the moment. And pressing Ctrl+Z 10 times is a bit cumbersome.



  • Hello,

    using CallCommand() is the same as pressing the button in the GUI. This means that if you use CallCommand(), the complete command is executed - including any undo-creation within that command.

    You find an overview over the undo system in the C++ documentation: Undo System Manual.

    best wishes,
    Sebastian



  • @s_bach

    Thanks for the reference manual, but I can't really see an answer to my question

    Forgive me for reiterating my question above:

    Is it safe to say that with the CallCommand() function, I cannot attain one Undo for the whole script? Or do you have suggestion in place other than using CallCommand() function?

    The reason is looking forward now, I'd probably use at least 10 CallCommand in a final version of the script that I am working at the moment. And pressing Ctrl+Z 10 times is a bit cumbersome.



  • Now we are at a point where angels fear to tread...

    I can't say what exactly the Start/EndUndo system is doing when not properly used in one level, but it definitely doesn't work in a way that supports nesting. Here I just tried a small modification of my previous scripts:

    import c4d
    
    def main():
        doc.StartUndo();
        obj = doc.GetFirstObject();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
    
        doc.StartUndo();
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        doc.EndUndo();
    
        doc.StartUndo();
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        doc.EndUndo();
    
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
    
        obj = obj.GetNext();
        doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, obj);
        obj.SetName("Renamed!");
        doc.EndUndo();
        c4d.EventAdd();
    
    if __name__=='__main__':
        main()
    

    I used the same sequence of five renames, but I nested rename 2+3 into a new Start/EndUndo without closing the outer first.
    If you apply Ctrl-Z on that, you will find that the first Ctrl-Z reverses the last three renames (NOT just the last 2), one more Ctrl-Z reverses the second rename, and one more the first. That means that an undo step goes back to the last StartUndo (not EndUndo) that the programmer has set. Any nesting is ignored.

    I did a few more nesting experiments but I'm not posting them here as they all point to the same behavior. I do not get any errors though, even if I completely botch the sequence - two StartUndo in a row, AddUndo before StartUndo, AddUndo after EndUndo... it actually still works, but with the little limitation that each StartUndo that I issue will break up the sequence and require a new Ctrl-Z to undo. (I am not going into full reverse engineering here now - it would be nice if the documentation could tell us how error cases and nesting are actually handled, though.)

    That means that if you have commands that internally perform an Start/EndUndo, you will not be able to get out of the additional Ctrl-Z. Same, I guess, if you issue a StartUndo inside of a function that you wish to reuse (although that may be a problem more suitable for libraries).

    You may argue that this is a design flaw. A nested Start/EndUndo could actually be treated like a single subcommand, with a Ctrl-Z working only on the highest level - which would revert the results of a functionality from the view of the outermost Start/EndUndo, while all nested Start/EndUndo pairs on lower levels do not affect the reversal (so you could use a CallCommand including its internal Undos without enforcing another Ctrl-Z in the end). I'm just not sure whether C4D could handle this conceptually, as it would be easy to lose yourself in a tree of Start/EndUndos throughout the program.

    Anyway. Your best bet may actually be to skip the CallCommands if possible and perform the necessary change by yourself, so you have a better control over the Undo sequences.
    Or you can issue a feature request for a CallCommand with an additional parameter that allows you to suppress the Start/EndUndos that are added internally.



  • Hello,

    as Cairyn pointed out, the undo system does not support nesting.

    So calling CallCommand() multiple times is the same as pressing multiple buttons in the GUI - it will create multiple undo steps. The only alternative is to not use CallCommand() but to do the specific operations (using the API) yourself.

    best wishes,
    Sebastian



  • @Cairyn

    Thanks again for the detailed explanation. I'll put this to rest. At least for now I know the limitation and the commands to avoid.

    @s_bach

    Thanks for the confirmation.

    Have a great day ahead!


Log in to reply