Fighting with UNDO
-
On 12/11/2015 at 01:25, xxxxxxxx wrote:
Hello,
I have a lot of problems to make my UNDO work in my Scripts.
I follow the Rules in the SDK and I put UNDOTYPE_NEW after a funtion
and UNDOTYPE_CHANGE before something is changed.
Still the UNDO does not work correctly..In my current Script the Undo even
makes one Undo to much. So it undos even a change I made in the scene
before I run the Script.Question:
I make a lot of Changes of Objects. Changing Visibility and selection in Objectmanager.
Do I need to make an Undo for any Change? Even If I change like 2 things in the Row like this:doc.AddUndo(c4d.UNDOTYPE_CHANGE, shrinkwrapdeformer) shrinkwrapdeformer[c4d.ID_BASEOBJECT_GENERATOR_FLAG]=False doc.AddUndo(c4d.UNDOTYPE_CHANGE, shrinkwrapdeformer) shrinkwrapdeformer.SetName("HB_RetopoProjector")
I also wonder why we even have to make all this Undo stuff? It would be much better if by default any function would take care about undo. I dont see a reason why you would not want to add a Undo for any change. Maybe Instead of AddUndo we could only have a RemoveUndo. Not sure if that makes sense.
But for many of my Scripts I have more work making the Undo work than writing the Script itself.
greetings, Holger -
On 12/11/2015 at 02:36, xxxxxxxx wrote:
Well, undos are a complicated thing. You don't want them to eat too many processor cycles or memory, so they must not be created arbitrarily. You also don't want the undo structure to get too complicated or too granular - the user should not need to press the Undo key twenty times after an action.
Simply spoken, functions cannot have a built-in undo because the overhead would be enormous and the granularity absurd. Remember that the API functions that you call are also the internal functions of C4D, so if every function would have some built-in undo, C4D would generate tons of undos internally, even in hierarchical calls (functions calling other functions calling more functions) because a function is not aware of the context it is called in.
That is why normally you have a structure like this (mind you, this is an abstract concept and not necessarily exactly the thing C4D does) :
User does something | | GUI responder (window, button, menu...) | | GUI-level function (takes care of undos) | | Internal functionality (multiple complex calls)
So, you have a dedicated layer for handling the GUI itself (including the abstraction Windows/Mac, as well as mutual dependencies of GUI elements), a dedicated layer for the undos (and notifications to the threads about changes, like EventAdd()), and a huge API of internal functions that don't need to worry about undos and can concentrate on providing functionality.
If you use CallCommand(), you are actually triggering functions on the GUI level, and have an Undo built-in (as far as supported).
If you write a script using the internal API, you do not have Undos, because these functions don't know what you want to achieve, where you are doing something temporary, or where it is necessary to keep data for a later restauration of an object during an undo. So, you are writing yourself a GUI-level function and are therefore responsible for setting correct undos.
-
On 12/11/2015 at 03:08, xxxxxxxx wrote:
Hi Holger,
I'm not sure, it would be a good idea to let C4D handle the undo-stuff automatically. While C4D could add a undo step for every change that is made, I'm pretty sure users wouldn't like that. Depending on the complexity of an operation, the user would have to do thousands of undo steps, to undo such operation.
How should C4D be able to detect the complexity of certain undo steps. The idea behind this system is, to give the developer the chance, to merge several operations into a single undo step (no automatism can do that), in the end leading to a better user experience.Now, for your code:
I assume, you are using StartUndo() and EndUndo() correctly.
With these two functions you define one single, arbitrarily complex undo step.But you are adding too many undo steps.
Basically with AddUndo(c4d.UNDOTYPE_CHANGE, op) you say, this object will be changed, regardless of the number of parameters you change. So in your situation, there's only one AddUndo() necessary. -
On 12/11/2015 at 03:13, xxxxxxxx wrote:
Now, how to use the Undos best? First, you have the bracket calls StartUndo() and EndUndo(). These simply define what range of single changes (AddUndo()s) will be reverted when the user presses the undo key. They must match, so if your code runs over a StartUndo, it must also run over an EndUndo sometime later. That means no early exits from a function that opens a StartUndo!
Then there are many types of AddUndo()s that can be used within such a StartUndo/EndUndo bracket. Each determines the scope of the undo - the amount of data that C4D needs to remember to revert the change. For example, UNDOTYPE_BITS will create storage only for the bits of an object, which uses very little space, while UNDOTYPE_CHANGE_SMALL stores all the parameters, and UNDOTYPE_CHANGE stores even hierarchies, so it takes up more space. You call these AddUndos before you make the actual change because this way, C4D knows what state to remember.
Now I don't know the actual C4D source code, but I guess you can imagine it like this:
- C4D gets an AddUndo for a certain object
- C4D thinks "Hey, this thing here is going to be changed! I must remember it the way it used to be!"
- C4D creates a copy of that object and puts it into its Undo list
- Then you make changes to that object.When the user presses the Undo key, C4D looks at its list, and goes through all the AddUndo nodes in reverse order from the last EndUndo to the StartUndo before it. Now it finds that AddUndo node, and restores the original object with this data. Once it reaches the StartUndo, it considers that Undo sequence to be done, and stops.
Any failure to match EndUndo and StartUndo will corrupt this list. Any use of the wrong AddUndo may cause C4D to store too little data for a complete reversal of the change. If you use an UNDOTYPE with a broader scope, you should be able to execute several changes to an object with only one AddUndo - but you need to watch out for functions that DO create undos by themselves.
Think about what you need in an undo to revert an action, and use the appropriate UNDOTYPE. Now I am not a Maxon programmer so maybe they can add to the matter (the documentation is giving some examples but more information is always welcome), but your example above should also work as
doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, shrinkwrapdeformer) shrinkwrapdeformer[c4d.ID_BASEOBJECT_GENERATOR_FLAG]=False shrinkwrapdeformer.SetName("HB_RetopoProjector")
But when in doubt, it may be best to test
-
On 12/11/2015 at 03:39, xxxxxxxx wrote:
Hi Cairyn and Andreas, thanks a lot for the explanation. I think I completely misunderstood the Undo.
Of course I dont want to "store" all the operations I make in the Script. I usually want to undo the complete Script. But than from what you said I understand that I dont need to use any AddUndo.
But that does not work.
When I create an Object for example and I press undo the Object remains.
For example this Code. If I Undo the Script the Null Object remains.import c4d
from c4d import guidef main() : obj=doc.GetActiveObject() NullObj=c4d.BaseObject(c4d.Onull) NullObj.InsertBefore(obj) c4d.EventAdd() if __name__=='__main__': main()
-
On 12/11/2015 at 04:14, xxxxxxxx wrote:
Sorry, Holger,
that was probably a misunderstanding.
You need a full correct sequence of StartUndo(), one or more AddUndo() and EndUndo() for undo to work.For your last piece of code, like so:
def main() : obj=doc.GetActiveObject() NullObj=c4d.BaseObject(c4d.Onull) doc.StartUndo() if obj is None: doc.InsertObject(NullObj) else: NullObj.InsertBefore(obj) doc.AddUndo(c4d.UNDOTYPE_NEW, NullObj) doc.EndUndo() c4d.EventAdd() if __name__=='__main__': main()
Here's one more complex example (note the comment in the beginning) :
import c4d from c4d import gui #Welcome to the world of Python # Undo examples; requires these selected objects # - a sphere # - a polygon object with some point selection # - a torus # - a video post effect # - an object named "child" and an object named "parent" def main() : objects = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_0) if len(objects) == 0: return sphere = None polyObject = None torus = None child = None parent = None for obj in objects: if obj.GetType() == c4d.Osphere: sphere = obj if obj.GetType() == c4d.Opolygon: polyObject = obj if obj.GetType() == c4d.Otorus: torus = obj if obj.GetName() == "child": child = obj if obj.GetName() == "parent": parent = obj doc.StartUndo() if sphere is not None: doc.AddUndo(c4d.UNDOTYPE_CHANGE_SMALL, sphere) sphere[c4d.PRIM_SPHERE_RAD] = 200.0 if polyObject is not None: doc.AddUndo(c4d.UNDOTYPE_CHANGE_SELECTION, polyObject) pSelect = polyObject.GetPointS() pSelect.DeselectAll() cube = c4d.BaseObject(c4d.Ocube) if cube is not None: doc.InsertObject(cube, None, None) doc.AddUndo(c4d.UNDOTYPE_NEW, cube) if torus is not None: doc.AddUndo(c4d.UNDOTYPE_DELETE, torus) torus.Remove() rd = doc.GetActiveRenderData() videoPost = rd.GetFirstVideoPost() if videoPost is not None: doc.AddUndo(c4d.UNDOTYPE_BITS, videoPost) videoPost.SetBit(c4d.BIT_VPDISABLED) if child is not None and parent is not None: doc.AddUndo(c4d.UNDOTYPE_HIERARCHY_PSR, child) child.Remove() doc.InsertObject(child, parent, None) doc.EndUndo() c4d.EventAdd() if __name__=='__main__': main()
-
On 12/11/2015 at 04:17, xxxxxxxx wrote:
um, that's not what I said at all...
You need StartUndo, EndUndo, and a certain number of AddUndos that cover the changes you make.
In certain cases, one AddUndo can cover more than one operation.
But you still need to tell C4D what you want to undo. -
On 12/11/2015 at 05:15, xxxxxxxx wrote:
Thanks Guys, I think I have just a wrong understanding of "Storing" Undos...If the function would be called RemoveUndo or IgnoreUndo it would be clearer to me. AddUndo for me means that the current State of an objects gets saved so if the user presses Undo this state will be recovered because its in the Undolist. But it seems its quite the other way around. If I AddUndo that means that the current state gets not added to internal Undolist.
NeverMind Guys...I will just plainly do it how its supposed to. If I have issues I guess I will just need to find the Problem and deal with it.Thanks again.
-
On 12/11/2015 at 05:29, xxxxxxxx wrote:
Hi Holger,
no, I think, you are still confused. Actually it is pretty much like you describe for AddUndo:
AddUndo for me means that the current State of an objects gets saved so if the user presses Undo this state will be recovered because its in the Undolist.
And it's NOT the other way round, so it shouldn't be called RemoveUndo(), either.
Perhaps check and play with the examples I posted.