Closed monkeez closed 2 years ago
Nice one. :) Yes, you've identified the only two paths I can think of, one of which was recently part of cmdx but got removed for a tirelessly complex reason.
There at least 4 reasons this isn't going to work.
MFnTransform.setRotatePivot
is that undoable? (No)transform
using the DGModifier, and you cannot create DG nodes like multMatrix
with the DagModifier
. And you can't know which node type belongs where until you try. Especially for non-native plug-in types. Which means you now need to (1) instantiate one modifier, (2) test it and (3) instantiate another one. Triple the performance overhead, or more.In my experience, (1) is of most importance. Being confident that what you perform is both undoable, and is going to be undone in the exact same order. As soon as you mess up the order, such as involving calls to cmds
, you not only risk breaking undo, and risk putting Maya in an unstable state (e.g. nodes in the outliner don't actually exist) but also risk a Maya crash. Too much of my life has been spent learning this the hard way haha. Undo/redo is a hard problem that is too easy to take for granted.
I did this too, and used it for the first 3 years of cmdx. It was great, until it wasn't. The 2 reasons are similar to above.
For (1) like with embedding a modifier with native calls like cmdx.createNode
, having what appears to be an undo block distills a false sense of security. If only one of the commands you use isn't undoable, you won't know until Maya crashes without telling you why and where.
For (2) it may still be solvable, I tried and failed many times but did not explore every venue.
The way I did it was to embed the MDagModifier
with the node being created, such that any call such as ["attr"] = 5
would first check whether one was embedded, and whether it was still active (whether doIt
had been called or not). And it worked great! Until you also consider that one of the main appeals of cmdx is the persistence of nodes. That myNode
is the same node even when it changes name or moves in a hierarchy. Even when the variable is discarded and is recovered via e.g. cmdx.ls()
.
That means the DagModifier
is also maintained, even after having been doIt()
which means its internal state is maintained which means Maya cannot safely destroy the node.
Even when the node is removed from the user-side, a node lives in Maya memory until all memory of it is released. Maya will not tell you whether the memory is released or not, and will happily crash or not crash at the least convenient time.
This is the PR where all of that got stripped in favour of the current system.
So! The challenge then is to overcome these two major concerns. I would still like to believe it's possible! But if there is one main advantage of explicitly calling mod.something()
it is that predictability. Knowing that anything mod.
can do is undoable. And anything you do outside of it is at-your-own-risk.
Haha that is a lot of drawbacks!
For (1) Yeah that is annoying not knowing whether something will be undoable or not. Is it even possible to solve this one?
For (2) I've implemented it in a different way, but I haven't tested it enough to know if it faces the same memory issues.
The safest way to test memory is to create a node from a plug-in, and try and unload that plug-in. The unload mechanism knows exactly what's going on and will tell you whether something is holding onto anything. I've only experimented with a custom plug-it, but I expect something like matrixNodes.mll
to also complain about it.
Is it even possible to solve this one?
Well, it is, yes. There's no great mystery to undo, the way cmdx implements it is by piggybacking on the modifier and calling undoIt
at an appropriate time. But undoIt is nothing other than calling the opposite of whatever was done.
We could put a decorator on every single function in cmdx that includes the opposite of that call. The way you pass this to the undo stack currenctly is by calling cmdx.commit(undo, redo)
. So even MFnTransform.rotatePivot
could technically also become undoable this way.
def myCustomRedoIt():
cmdx.createNode()
cmdx.setAttr()
cmdx.connect()
# blabla
def myCustomUndoIt():
cmdx.disconnect() # Must happen before we delete!
# cmdx.setAttr() # Doesn't need to be undone since we're deleting the node (in this case!)
cmdx.delete() # Order is important
myCustomRedo()
cmdx.commit(myCustomUndoIt, myCustomRedoIt)
Anything could be put in these two calls, this is all of what undo/redo is. Nothing is truly "undone". Every command simply has an opposite. For setting attributes, the challenge is remembering the previous value, somehing the modifiers do a good job at and is easy to take for granted.
Is it worth it? Maaaybe.. You tell me.
Is it worth it? Maaaybe.. You tell me.
My initial thought is probably no.. but I think I'll poke around some more.
I can somewhat imagine a future where cmdx could on the front page state "Everything is undoable".
The number of calls to account for is large, but not infinite. If that could be made possible without breaking the readability of the code - like decorators is a good idea - and with it being explicit and optional, such that we could still write plug-ins using cmdx, it might be worth it. If it's explicit, it could potentially be made available piecewise, before everything is actually done.
Off the top of my head, if there was a..
with cmdx.UndoChunk():
cmdx.createNode() # Ok
cmdx.setAttr() # ERROR: Not yet implemented
That would probably be OK. So long as we can trust it.
Another advantage of the modifiers is just that, chunks of commands are made explicit. It means you can currently do things like..
with cmdx.DagModifer() as mod:
mod.createNode()
mod.connect(badNode, otherBadNode)
And it'll error out on that second call, before actually calling the first. Meaning Maya can never (theoretically) end up in a bad state, where a function fails half-way through leaving a big old mess behind. It'll either succeed completely, or do nothing and tell you why.
Actually continuing on this, one thing I would like to encapsulate like this is also evaluation.
with cmdx.DagModifier() as mod:
node = mod.createNode("transform")
mod.setAttr(node["tx"], otherNode["tx"].read())
Currently, that call to .read()
will trigger an evaluation of whatever otherNode
needs in order to provide "tx"
. This is one of my current headaches since there can be calls upstream that effect evaluation in unexpected ways. Like, maybe otherNode
is parented to another node which is parent constrained to yet another node which is like a follicle, which is driven by a mesh somewhere and the mesh is computed by whatever else. Suddenly, the command behaves unpredictably depending on the frame you are on, or whether there is an unrelated cycle, and depending on whatever else happens within that mod block.
My current workaround is something like this.
# Somewhere upstream
cache = {otherNode: {"tx": otherNode["tx"].read()}}
...
do_the_thing(node, otherNode, cache)
Where do_the_thing
isn't calling otherNode["tx"]
but rather cache[otherNode]["tx"]
.
If this could be encapsulated also, I would be a very happy man. Outside the scope of undo of course, just making note.
Closing this for now. If you or anyone else find yourself poking around some more, this is still an interesting area of research and new PRs are welcome.
Hey after talking about undo in the other PR I was messing around trying to get undo to work for the nicer syntax. I've come up with something that seems to work, but there's probably some huge flaw that I'm missing. Take a look and see if you think it's worth pursuing.
Undo without using modifiers directly
Using modifiers to group commands for undo
Using modifiers the old way works too
How it works
Commands that edit the DG are wrapped by the
@use_modifier
decorator. This decorator gets the active modifier (DGModifier
orDagModifier
) if there is one, otherwise creates a new one for the operation. It then inserts the modifier into the_mod
keyword argument, the function can then call modifier methods via_mod
.Limitations
Querying attrs after setting them inside a modifier won't give you up to date information (just like before when using a modifier).
Undo is more expensive than no undo 😄, the main cost comes from having to create new instances of
DGModifier
orDagModifier
. The cost of this makes creating new nodes more in line with the speed ofcmds.createNode
.However, most of the cost can be avoided by calling from within a
DagModifier
orDGModifier
block.If you take a look at the
test_performance.py
file I've wrapped the test in a modifier, and you can see that the performance tests still pass without changing the comparison value tocmds
ormaya.api.OpenMaya
.