MNodeMessage.addAttributeChangedCallback on maya 8.5

156 views
Skip to first unread message

Chadrik

unread,
Mar 10, 2009, 3:01:20 PM3/10/09
to python_inside_maya
this callback seems to do absolutely nothing on maya 8.5. without it
there will be no API-undo for maya 8.5 in the new pymel. does anyone
know any workarounds? unfortunately, maya.cmds.scriptJob won't do
because it has to be API.

-chad


Sebastian Thiel

unread,
Mar 10, 2009, 3:18:15 PM3/10/09
to python_in...@googlegroups.com
Could you briefly explain how the PyMel API Undo is working and what it is based on ?
How do you separate attribute changes made by MEL ( and thus queued by MEL as well ), made by a plugin or made by a PyMel function ? How can a callback based system integrate into maya's existing undo queue ( without tracking things multiple times ) ?

Thanks for a quick reply,
Sebastian

chadrik

unread,
Mar 10, 2009, 3:40:27 PM3/10/09
to python_in...@googlegroups.com
it's based on a clever prototype that Dean Edmonds posted on this site
awhile back. it works like this:

- using the API, create a garbage node with an integer attribute,
lock it and set it not to save with the scene.
- add an API callback to the node, so that when the special attribute
is changed, we get a callback
- the API queue is a list of simple python classes with undoIt and
redoIt methods. each time we add a new one to the queue, we increment
the garbage node's integer attribute using maya.cmds.
- when maya's undo or redo is called, it triggers the undoing or
redoing of the garbage node's attribute change (bc we changed it using
MEL/maya.cmds), which in turn triggers our API callback. the callback
runs the undoIt or redoIt method of the class at the index taken from
the numeric attribute.

pretty ingenious. much thanks to Dean for that one.

so, the API provides many methods which are pairs -- one sets a value
while the other one gets the value. the naming convention of these
methods follows a fairly consistent pattern. so what I did was
determine all the get and set pairs, which I can use to automatically
register api undo items: prior to setting something, we first *get*
it's existing value, which we can later use to reset when undo is
triggered.

so, this API undo is only for PyMEL methods which are derived from API
methods. it's not meant to be used with plugins. and since it just
piggybacks maya's MEL undo system, it won't get cross-mojonated.


-chad

Sebastian Thiel

unread,
Mar 10, 2009, 4:15:20 PM3/10/09
to python_in...@googlegroups.com
This appears to me like an 'evil hack' ( which is how I rarely annotate my code to workaround things ;) ) - and such a hack should not be the basis of a whole undo framework.

What happens with your internal queue if the user flushes undo ? ( alternatively maya flushes it when creating a reference, or when unloading it, or removing it ). To me it appears the index on the garbage node would stay as it was, as well as your internal queue. Perhaps you can register a callback that will always be triggered in such cases, so you can react to it.

When the user changes scenes, you will loose your previous garbage node, and have to create a new one. Perhaps you could set the node shared, or add yet more callbacks, to be notified in such cases.

Just to get this node work right one has to do whole lots of work and catch many special cases - all this could possibly break in yet another unforseen situation.


What happens if I trigger one undoable pymel API command, that internally calls 10 more undoable commands ( and we could go on like it ). The user should be able to undo it with just one undo step, but your node got changed 11 times in the meanwhile, causing 11 operations to be put onto the undo stack. To fully undo, you would have to undo all 11 changes.
Even if it would not fill up maya's undo queue, it would be impossible to actually undo the right thing ( rendering the whole undo system unusable ).


What do you think about the flagged issues ?

chadrik

unread,
Mar 10, 2009, 4:51:06 PM3/10/09
to python_in...@googlegroups.com
sebastian

This appears to me like an 'evil hack' ( which is how I rarely annotate my code to workaround things ;) ) - and such a hack should not be the basis of a whole undo framework.

What happens with your internal queue if the user flushes undo ? ( alternatively maya flushes it when creating a reference, or when unloading it, or removing it ). To me it appears the index on the garbage node would stay as it was, as well as your internal queue. Perhaps you can register a callback that will always be triggered in such cases, so you can react to it.

i just looked up the code, and it does not use the index, but merely moves undoItems between the undo and redo stack:

    def _attrChanged(self, msg, plug, otherPlug, data):
        if self.cb_enabled \
           and (msg & _api.MNodeMessage.kAttributeSet != 0) \
           and (plug == self.cmdCountAttr):

            if _api.MGlobal.isUndoing():
                cmdObj = self.undo_queue.pop()
                cmdObj.undoIt()
                self.redo_queue.append(cmdObj)

            elif _api.MGlobal.isRedoing():
                cmdObj = self.redo_queue.pop()
                cmdObj.redoIt()
                self.undo_queue.append(cmdObj)




When the user changes scenes, you will loose your previous garbage node, and have to create a new one.

undo does not span scenes anyway, so this should not be an issue, right?




What happens if I trigger one undoable pymel API command, that internally calls 10 more undoable commands ( and we could go on like it ).

undoable pymel API commands are atomic -- they never call anything but pure api.

Just to get this node work right one has to do whole lots of work and catch many special cases - all this could possibly break in yet another unforseen situation.
What do you think about the flagged issues ?


it ain't pretty, but it works. there may end up being special cases to address, but so far we have not hit any.  the benefits you get from adding the API as an option for pymel to delegate to FAR outweigh the unaesthetic nature of this undo solution.  in the end, these API calls are *additional* methods that you can use if you want, or not.  the docstrings on each method tell you whether it is derived from MEL or from API, and if from API, if it is undoable.  the most important and complex methods still derive from maya.cmds: things like setAttr, addAttr.

the flushing of maya undo i'll have to look into.  not sure if there's an API callback for that....

 we've been using this new API/MEL hybrid pymel for about 3-4 months now and the TDs here, including myself, are loving the hybridization.  it feels less like a prototype now and more like something that ties into maya at a core level : because that's exactly what it does :) .  it's awesome for prototyping plugins too, as you can get API classes for any PyNode.  From the new docs:


PyNode classes now derive their methods from both MEL and the API ( aka. maya.cmds and maya.OpenMaya, respectivelly ).  If you're  familiar with Maya's API, you know that there is a distinct separation between objects and their abilities.  There are fundamental object types such as MObject and MDagPath that represent the object itself, and there are "function sets", which are classes that, once instantiated with a given fundamental object, provide it with special abilities.  ( Because I am a huge nerd, I like to the think of the  function sets as robotic "mechs" and the fundamental objects as "spirits" or "ghosts" that inhabit them, like in *Ghost in the Shell* ). 

For simplicity, pymel does away with this distinction: a PyNode instance is the equivalent of an activated API function set;  the necessary fundamental API objects are determined behind the scenes at instantiation.  You can access these by using the special methods __apimobject__, __apihandle__, __apimdagpath__, __apimplug__, and __apimfn__.  ( Be aware that this is still considered internal magic, and the names of these methods are subject to change ):

    >>> p = PyNode('perspShape')
    >>> p.__apimfn__() # doctest: +ELLIPSIS
    <maya.OpenMaya.MFnCamera; proxy of <Swig Object of type 'MFnCamera *' at ...> >
    >>> p.__apimdagpath__() # doctest: +ELLIPSIS
    <maya.OpenMaya.MDagPath; proxy of <Swig Object of type 'MDagPath *' at ...> >
    >>> a = p.focalLength
    >>> a
    Attribute(u'perspShape.focalLength')
    >>> a.__apimplug__() # doctest: +ELLIPSIS
    <maya.OpenMaya.MPlug; proxy of <Swig Object of type 'MPlug *' at ...> >

As you can probably see, these methods are enormously useful when prototyping API plugins.

-chad








Sebastian Thiel

unread,
Mar 10, 2009, 5:27:37 PM3/10/09
to python_in...@googlegroups.com
On Tue, Mar 10, 2009 at 9:51 PM, chadrik <cha...@gmail.com> wrote:
sebastian

This appears to me like an 'evil hack' ( which is how I rarely annotate my code to workaround things ;) ) - and such a hack should not be the basis of a whole undo framework.

What happens with your internal queue if the user flushes undo ? ( alternatively maya flushes it when creating a reference, or when unloading it, or removing it ). To me it appears the index on the garbage node would stay as it was, as well as your internal queue. Perhaps you can register a callback that will always be triggered in such cases, so you can react to it.

i just looked up the code, and it does not use the index, but merely moves undoItems between the undo and redo stack:

    def _attrChanged(self, msg, plug, otherPlug, data):
        if self.cb_enabled \
           and (msg & _api.MNodeMessage.kAttributeSet != 0) \
           and (plug == self.cmdCountAttr):

            if _api.MGlobal.isUndoing():
                cmdObj = self.undo_queue.pop()
                cmdObj.undoIt()
                self.redo_queue.append(cmdObj)

            elif _api.MGlobal.isRedoing():
                cmdObj = self.redo_queue.pop()
                cmdObj.redoIt()
                self.undo_queue.append(cmdObj)




When the user changes scenes, you will loose your previous garbage node, and have to create a new one.

undo does not span scenes anyway, so this should not be an issue, right?

It does not span scenes, but you will require a new garbage node. As you do not know whether you even need one ( as the user could never any API commands, you would have to check for the existence of the node each time someone calls the API through undoable pymel.
Alternatively, you assure you always have a node in the scene, and this is exactly what I meant.
 




What happens if I trigger one undoable pymel API command, that internally calls 10 more undoable commands ( and we could go on like it ).

undoable pymel API commands are atomic -- they never call anything but pure api.

Yes, but Method A sets tx, ty and tz of a transform - that is 3 calls to the API , but just one call to MethodA. The way it works now with that node, it would 3 operations onto the stack, requireing it to be undone 3 times ( instead of one ).
Your explanation does not alleviate my concern - which is acutally a major flaw in the whole concept of using a garbage node.
In fact you would have to store these 3 commands in an individual list that can be undone at once. This is impossible with a garbage node putting every attribute change onto maya's undo stack. Even if you would track your stack depth to be able to internally track your 3 commands as one, maya's undo queue will still be contaminated with 2 additional do-nothing commands.
This shows that having a node respond to every API call ( that should be undoable ) cannot work the way undo has to work.


Just to get this node work right one has to do whole lots of work and catch many special cases - all this could possibly break in yet another unforseen situation.

What do you think about the flagged issues ?


it ain't pretty, but it works. there may end up being special cases to address, but so far we have not hit any.  the benefits you get from adding the API as an option for pymel to delegate to FAR outweigh the unaesthetic nature of this undo solution.  in the end, these API calls are *additional* methods that you can use if you want, or not.  the docstrings on each method tell you whether it is derived from MEL or from API, and if from API, if it is undoable.  the most important and complex methods still derive from maya.cmds: things like setAttr, addAttr.

 
 

the flushing of maya undo i'll have to look into.  not sure if there's an API callback for that....

 we've been using this new API/MEL hybrid pymel for about 3-4 months now and the TDs here, including myself, are loving the hybridization.  it feels less like a prototype now and more like something that ties into maya at a core level : because that's exactly what it does :) .  it's awesome for prototyping plugins too, as you can get API classes for any PyNode.  From the new docs:

I agree 100% that using and mixing the API is a great thing, but still I do not believe that this approach to getting undo into the API methods will be working as desired.
Perhaps I am all too theoretical, and it will work great and flawless in practice.
But: Currently your system tracks every hooked-up API call and put's it onto maya's undo queue, but only the top-level call should be queued there. All 'internal' calls the top level call has made must be tracked and undone once your single top level call is being undone.

Good Luck,
Sebastian


chadrik

unread,
Mar 10, 2009, 8:00:48 PM3/10/09
to python_in...@googlegroups.com


Yes, but Method A sets tx, ty and tz of a transform - that is 3 calls to the API , but just one call to MethodA. The way it works now with that node, it would 3 operations onto the stack, requireing it to be undone 3 times ( instead of one ).

i think this is where you are misunderstanding.  let me clarify a little more how it all works with an example.  take MFnTransform.setTranslation. PyMEL provides a wrapped copy of this as Transform.setTranslation.   when pymel.Transform.setTranslation is called, here's what happens in relation to undo:

1) process input args, if any
2) call MFnTransform.getTranslation() to get the current translation. 
3) append to the api undo queue, with necessary info to undo/redo later (the current method, the current args, and the current translation)
4) call MFnTransform.setTranslation() with the passed args
5) process result and return it

Transform.setTranslation does not call any API methods other than those pertaining to translation.   so a call to setTranslation would append one element to the undo stack.  it doesn't matter that internally, translation is three attributes, all that is handled in the c++ api.  it's still one api call, which needs just one undo.



Your explanation does not alleviate my concern - which is acutally a major flaw in the whole concept of using a garbage node.
In fact you would have to store these 3 commands in an individual list that can be undone at once. This is impossible with a garbage node putting every attribute change onto maya's undo stack. Even if you would track your stack depth to be able to internally track your 3 commands as one, maya's undo queue will still be contaminated with 2 additional do-nothing commands.
This shows that having a node respond to every API call ( that should be undoable ) cannot work the way undo has to work.

first of all, a little primer on how undo works.  let's start with some example code:



def check():
for cam in cmds.ls(type='camera'): 
print cam, cmds.getAttr( cam + ".focalLength" )

for cam in cmds.ls(type='camera'): 
cmds.setAttr( cam + ".focalLength", 20 )

--------------STOP---------------------

check()
cmds.undo()
check()



if you run the first part, you set all the focal lengths in the scene to 20.  if you're starting from a fresh scene. that is 4 separate undo items, because there's four cameras.  maya is smart about this and it knows to group them because they were all done in a "for" loop.  so if you call cmds.undo a single time, as in the example above, it will undo all 4 at once, but it's still 4 separate undos in a stack  ( to see this for yourself, you can add an attribute change callback to all the cameras and see that calling cmds.undo once in the example above changes each attribute in succession).  to further demonstrate that this grouping effect is maya magic, try the same thing via a standalone interpreter:  it doesn't work.  cmds.undo in standalone will only undo one setAttr at a time, even if they're done in a loop.

here's the pymel equivalent.  if you check out the latest pymel from svn and run this in 2009, it will work just the same as the maya.cmds example above.  




for cam in ls(type='camera'): 
cam.setFocalLength( 20 )

--------------STOP---------------------

check()
undo()
check()


it undoes everything as expected.  i hope that this clarifies how the system works. in fact, it works very much the way that undo/redo works when writing plugins, only it's external to the plugin API.  in that regard, i think it's less of a hack than you  make it out to be.  

all this said, i think that Autodesk needs to consider opening up the undo queue for us hackers to utilize outside of the plugin architecture.  now that the API is python, we don't always want to use it in a plugin or in a node.  but in case you haven't noticed, the entire point of pymel is that we don't have time to wait for autodesk to catch up.   

i'm glad that you're pushing me on this issue, because it helped me clarify my understanding of the undo queue and i squashed some bugs along the way, but rest assured that it does work.


-chad



Sebastian Thiel

unread,
Mar 11, 2009, 3:48:23 AM3/11/09
to python_in...@googlegroups.com
Good, so I will rest in peace on this one :). Thanks for taking the time to disperse my doubts.
I will try to break it , lets see how I do ;).

Regards,
Sebastian

Sebastian Thiel

unread,
Mar 11, 2009, 4:28:48 AM3/11/09
to python_in...@googlegroups.com
Great, my doubts are completely dispersed now as maya nicely does the magic you mentioned. It was also possible to mix the wrapped API with obvious mel ( using mel.x() directly ).
Cheers,
Sebastian
Reply all
Reply to author
Forward
0 new messages