Universal "Undo" to Rule them All?



  • @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