[Maya-Python] API Access to Undo Queue

2,374 views
Skip to first unread message

Marcus Ottosson

unread,
Feb 20, 2018, 7:00:01 AM2/20/18
to python_in...@googlegroups.com

Hi all,

I’m looking to integrate undo into a series of commands made via the API.

from maya.api import OpenMaya as om

mod = om.MDagModifier()
mod.createNode("transform")
mod.doIt()

Maya is very kind to include the mod.undoIt() command, but it doesn’t enjoy calling it when being asked to Undo, such as via the Edit -> Undo menu item.

Google revealed that this topic has been discussed quite a few times in the past with little to no avail.. The suggested methods seem to boil down to either..

  1. Don’t use the API
  2. Write a Command Plug-in

Let’s assume for a second that (1) is a no-go, then what about (2)? That would work in situations where you had a pre-defined command you needed to run multiple times in a pre-defined fashion; like the cmds.polySphere command that creates a few nodes, makes a few connections and sets a few attributes.

But what if you just wanted to run something like the above? What if the number of commands you had in mind are many, or depend on too many factors to embed into a plug-in? Is there no hope?

What I’m hoping for is something along these lines..

queue = om.MGlobalUndoQueue()
queue.append(mod.undoIt)

Such that when the user attempts to undo, this command is called, followed by whatever else was in the queue at the time of appending.

It doesn’t have to be this of course. So long as I tailor what is going to be undone when undo is on the march.

Any ideas?

Message has been deleted

Marcus Ottosson

unread,
Mar 15, 2018, 4:47:08 AM3/15/18
to python_in...@googlegroups.com

Still haven’t found a reasonable solution to this..

At this time, it looks like I’ll need to stick with maya.cmds for anything that may need undoing, which is effectively anything that does more than just read from Maya.

Sure there aren’t any ideas out there?

Matteo Di Lena

unread,
Mar 15, 2018, 6:28:41 AM3/15/18
to Python Programming for Autodesk Maya
It popped up in the latest cult of rig stream on twitch, season one episode 26, that's the quote from Raf:

"You need a custom command wrapper, you then have a dummy command and you create instances of it with a chunk of data on the fly". There wasn't an example on stream because he was using cmds to speed up things, but it makes sense, that line with the command will go into the undo queue and undo what's passed inside of it. Still have to give it a try though, not sure how it really works :)

Hope this helps!

Marcus Ottosson

unread,
Mar 15, 2018, 7:17:46 AM3/15/18
to python_in...@googlegroups.com
Thanks for the tip, hadn't seen that stream before! Any idea on where in the episode he talks about it?

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/python_inside_maya/1e61c343-5a80-4b87-977f-af4422128b15%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Matteo Di Lena

unread,
Mar 15, 2018, 10:33:29 AM3/15/18
to Python Programming for Autodesk Maya
https://www.twitch.tv/videos/238431349

around 48:40 on the twitch version of the episode. He didn't expand more on that topic as he wasn't planning on using it for now.

cheers!

Matteo Di Lena

unread,
Mar 15, 2018, 11:08:00 AM3/15/18
to Python Programming for Autodesk Maya
here's the youtube video starting from the minute it talks about it, for future reference as I just remembered that the twitch stream will go offline in a couple of weeks.

justin hidair

unread,
Mar 17, 2018, 4:16:10 PM3/17/18
to Python Programming for Autodesk Maya
There could be a tone of answers to this problem but I think the : "What if the number of commands you had in mind are many, or depend on too many factors to embed into a plug-in?"
needs more explanation , because when I see you coming with "createNode('transform')" , I'm indeed telling myself that you just need maya.cmds

Marcus Ottosson

unread,
Mar 17, 2018, 6:25:38 PM3/17/18
to python_in...@googlegroups.com

Consider a module that exposes members similarly to maya.cmds, such as createNode and connectAttr, but calls the Python API under the hood for performance. And consider having these members exposed not via flat functions like maya.cmds, but as objects like PyMEL. E.g. myNode["myAttr"] >> myOtherNode["yourAttr"]. Now consider expanding on what is made possible by both PyMEL and cmds, such as myNode["Distance", Kilometers] = myOtherNode["Age", Cached].

That is all fine and well, except none of it can be undone. :(

justin hidair

unread,
Mar 17, 2018, 6:32:30 PM3/17/18
to python_in...@googlegroups.com

When dark things come up like that , maybe the issue is not maya or undos but the design of your workflow / tools , maybe it needs to be simplified to stay aight with Maya , but yeah besides that if you need it fur your life it seems like you are pretty much f*cked lol

 

Sent from Mail for Windows 10

--

You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.

To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_m...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/python_inside_maya/CAFRtmOBzCaKuv8hkKZYa1WxxdMfidbBXSzaYWaAVtR9iky2x_g%40mail.gmail.com.

Marcus Ottosson

unread,
Mar 17, 2018, 6:44:17 PM3/17/18
to python_in...@googlegroups.com
Thanks for your contribution, Justin.​

Marcus Ottosson

unread,
Mar 25, 2018, 7:54:10 AM3/25/18
to python_in...@googlegroups.com

I’ve put together my impression of what was mentioned in the cult of rig video posted earlier.

Example

from maya.api import OpenMaya as om
import apiundo

mod = om.MDagModifier()
mod.createNode("transform")
mod.doIt()

apiundo.commit(
    undo=mod.undoIt,
    redo=mod.doIt
)

However I’m struggling to properly merge it with Maya’s native undo queue; the main contender is redo. Natively, once something has been redone, you can undo it again. I haven’t yet managed to find a generic method of achieving this. Other than that it remains largely untested. Could use some help on this one.

Serguei Kalentchouk

unread,
Mar 25, 2018, 10:12:06 AM3/25/18
to python_in...@googlegroups.com
Hey Marcus,

For starters you shouldn't be popping undo/redo methods otherwise it immediately gets lost after you undo.
Ideally, you don't want to be managing your own stack anyway since the command object is already on the Maya stack you can have it own the relevant undo/redo method, the only question is how to ingest it in the least offensive way possible.

Here is one possible way of doing it:

# modify your code with the following
import _ctypes

class apiUndo(om.MPxCommand):
    def doIt(self, args):
        # using asDouble, asInt fails here probably because id is too large
        undo_id = long(args.asDouble(0))
        redo_id = long(args.asDouble(1))
        self.undo = _ctypes.PyObj_FromPtr(undo_id)
        self.redo = _ctypes.PyObj_FromPtr(redo_id)

    def undoIt(self):
        self.displayInfo("Undoing..")
        self.undo()

    def redoIt(self):
        self.displayInfo("Redoing..")
        self.redo()


# in maya define undo/redo somehow
def undo():
    print 'Undo Me'

def redo():
    print 'Redo Me'

# call command directly
cmds.apiUndo(id(undo), id(redo))

Cheers!


--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/python_inside_maya/CAFRtmOAmLLdnKHFW%3DzQPuZMDu67XNhH8QYHmPwXme-zfhBsQGA%40mail.gmail.com.

Marcus Ottosson

unread,
Mar 25, 2018, 11:20:14 AM3/25/18
to python_in...@googlegroups.com

That’s a good idea!

For starters you shouldn’t be popping undo/redo methods otherwise it immediately gets lost after you undo.

Indeed, was hoping to find a way around that, and it looks like what you’ve got here should work. Didn’t know the command class was instantiated per call, that is really helpful, as it means I can store stuff in the instance and let Maya keep track of the order of calls itself.

Here is one possible way of doing it:

Aw shucks. Doesn’t appear to work on Windows..

# Traceback (most recent call last):
#   File "<maya console>", line 1, in <module>
#   File "<string>", line 2, in apiUndo
# RuntimeError: Cannot convert argument 0 to float.
# # Traceback (most recent call last):
# #   File "C:/.../apiundo/apiundo.py", line 73, in doIt
# #     undo_id = long(args.asDouble(0))
# # TypeError: Cannot convert argument 0 to float. #

Was also a little weary of _ctypes.

However I managed to remove the need to pop at least, by storing undo/redo in the shared member, and passing them to the instance of the command class like in your example!

Thanks Serguei, making progress!

Serguei Kalentchouk

unread,
Mar 25, 2018, 11:45:12 AM3/25/18
to python_in...@googlegroups.com
Hey Marcus,

You can try going through the string then, ex:

# in plugin
def doIt(self, args)
    undo_id = long(args.asString(0), 0)
    ...

# in the call
cmds.apiUndo(hex(id(undo)))

There is nothing wrong with using _ctypes :)
This method will work in CPython implementation such as Maya.

Cheers!


--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

Marcus Ottosson

unread,
Mar 25, 2018, 11:53:08 AM3/25/18
to python_in...@googlegroups.com

Ooo nice. :)

Out of these two options - one retrieving an object from memory via ctypes and the other via a shared object - which do you prefer? The shared object pollutes sys.modules whereas retrieving from memory requires some explanation, and is potentially a few cycles more expensive?

Serguei Kalentchouk

unread,
Mar 25, 2018, 12:18:17 PM3/25/18
to python_in...@googlegroups.com
I would personally prefer a more direct coupling, otherwise the command, if invoked by itself, does nothing or potentially can pick up stale cached values from the shared location.
The pointer approach just felt more direct to me since my command wrapper was implemented in a C++ plugin.

Cheers!

On Sun, Mar 25, 2018 at 11:53 AM, Marcus Ottosson <konstr...@gmail.com> wrote:

Ooo nice. :)

Out of these two options - one retrieving an object from memory via ctypes and the other via a shared object - which do you prefer? The shared object pollutes sys.modules whereas retrieving from memory requires some explanation, and is potentially a few cycles more expensive?

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

Marcus Ottosson

unread,
Mar 25, 2018, 12:26:23 PM3/25/18
to python_in...@googlegroups.com

if invoked by itself

Good point. It’d need to be prevented from being called explicitly somehow, and only be used via this “commit” function.

Will experiment with these approaches. Thanks!

Marcus Ottosson

unread,
Mar 25, 2018, 3:36:16 PM3/25/18
to python_in...@googlegroups.com

I’ve ran into two issues with the ctypes approach so far.

  1. long is deprecated in Python 3; but can be worked around
  2. Garbage collection

I’ve put together a reproducible of the issue; TLDR; it (sometimes) crashes Maya.

import _ctypes
from maya.api import OpenMaya as
 om

ids = dict()

def createBox():

    mod = om.MDagModifier()
    mod.createNode("transform"
)
    mod.doIt()

    def undo():
        mod.undoIt()

    ids["undo"] = hex(id(undo))

createBox()

# At this point, `undo` has been garbage collected
undo = _ctypes.PyObj_FromPtr(long(ids["undo"], 0))

# WARNING: This *may* crash Maya
undo()

What I suspect happens is that _ctypes happily returns something that you might expect to be the local undo(), and lets you call it. Presumably because it is forcibly returned as a callable, regardless of what is actually at that memory location. In reality it probably couldn’t return that function, because it no longer exists due to garbage collection.

Any ideas?

Matteo Di Lena

unread,
Mar 25, 2018, 3:37:32 PM3/25/18
to Python Programming for Autodesk Maya
Hey guys,

thanks a lot for keeping this thread alive, it's something that I've always heard about in forums or videos, but no one ever got into details of implementing it. It's really nice of you to share your discoveries :)

Cheers!

Serguei Kalentchouk

unread,
Mar 25, 2018, 6:20:18 PM3/25/18
to python_in...@googlegroups.com
Hi Marcus,

In Python 3 casting to int should work just fine.

Yes it is possible to end up in a situation where your object has been garbage collected and thus an ID is now pointing to invalid memory.
So storing IDs for objects allocated on the function stack definitely isn't safe in this case, and definitely not ok to call a nested function from outside the parent function scope, regardless!

PyObj_FromPtr does increments the reference counter and therefore should prevent the GC from trashing your object, however, care still needs to be taken to make sure it gets executed before GC can have a chance to do so.

So in this example, the custom command wrapper that ends up calling the _ctypes.PyObj_FromPtr needs to be called from within the createBox function and it should probably pass either the mod class instance itself or another custom class with undo/redo methods it can call.
As a side note, I found that calling PyObj_FromPtr on a class methods (like mod.undoIt), to be unreliable, but the following workaround seems to work:

mod = om.MDagModifier()
undoIt = mod.undoIt
undoItRef =_ctypes.PyObj_FromPtr(id(undoIt))

Also, this is not production proven code so of course all the usual caveats apply :)

I wrote a quick post about this:

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

Marcus Ottosson

unread,
Mar 26, 2018, 3:44:00 AM3/26/18
to python_in...@googlegroups.com

thanks a lot for keeping this thread alive

Welcome. :) Not having found the information elsewhere is reason enough to make the extra effort to see it solved once and for all, hoping it’ll pop up when the next person Google’s something like it.

On that note, @serguiei, would it be possible to link back to this thread from your blog? Direct link


To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsubscribe@googlegroups.com.

--
You received this message because you are subscribed to the Google Groups "Python Programming for Autodesk Maya" group.
To unsubscribe from this group and stop receiving emails from it, send an email to python_inside_maya+unsub...@googlegroups.com.

Marcus Ottosson

unread,
Apr 3, 2018, 7:15:05 AM4/3/18
to python_in...@googlegroups.com

Happy to report this has worked surprisingly well!

Using it primarily with the modifier, like in the example above. In fact, I subclassed the modifier to automatically add itself to the undo queue when used.

from maya.api import OpenMaya as om
import apiundo

class DagModifier(om.MDagModifier):
    def __init__(self, *args, **kwargs):
        super(DagModifier, self).__init__(*args, **kwargs)
        apiundo.commit(self.undoIt, self.doIt)

mod = DagModifier()
mod.createNode("transform")
mod.doIt()

At first it seemed a little too simple to actually work in practice, but sometimes the simple things work best, and so it’s been!

There is however one caveat that I’ve encountered so far with apiundo in general, which is that undoing the creation of a node derived from a custom plug-in prevents that plug-in from being unloaded; even after calls to cmds.flushUndo() and cmds.file(new=True). I suspect the instance of the Maya command is being held in memory by Python along with the reference to the modifier responsible for creating the node, but have yet to thoroughly investigate. I did try storing references to functions with weakref.ref(), but even that didn’t make a difference.

The “workaround” is restarting Maya, which is somewhat of a pain so any help with this would be much appreciated.

Accepting pull-requests!

Ps. The above example works, but is somewhat simplified. Don’t forget the edge case of it being added to the undo-queue even withing calling doIt, and if you do subclass doIt instead, don’t forget to add the superclass-doIt to the undo queue.

Reply all
Reply to author
Forward
0 new messages