In the context of command plug-ins, object creation and manipulation is intimately tied to Maya's undo/redo system. In fact, commands which manipulate the state of the Dependency Graph and its nodes (which includes the DAG) should implement an undo operation to reverse the action to ensure that Maya's internal state remains in sync with its undo stack. In this topic, we illustrate how to create and manipulate objects within an undoable command.
We begin by presenting the structure of an undoable command. Later in this topic, we will present an undoable command which creates and manipulates a camera.
Python API 2.0:
# pySampleUndoableCommand.py
import sys
import maya.api.OpenMaya as OpenMaya
# ... additional imports here ...
def maya_useNewAPI():
"""
The presence of this function tells Maya that the plugin produces, and
expects to be passed, objects created using the Maya Python API 2.0.
"""
pass
kPluginCmdName = 'myUndoableCommandName'
##########################################################
# Plug-in
##########################################################
class MyUndoableCommand( OpenMaya.MPxCommand ):
def __init__(self):
''' Constructor. '''
OpenMaya.MPxCommand.__init__(self)
def doIt(self, args):
''' doIt() is called once when the command is first executed. '''
# ... Perform any object creation here, since doIt() is only called once per command instance ...
# Call self.redoIt() to perform the command's actual work. This function call flow
# is useful for code re-use.
self.redoIt()
def redoIt(self):
''' redoIt() is called every time the instance of this command is re-done from
the undo queue. '''
# ... Perform any object manipulation here ...
pass
def undoIt(self):
''' undoIt() is called every time the instance of this command is undone. '''
# ... Reverse any object creation or manipulations here ...
pass
def isUndoable(self):
''' This function indicates whether or not the command is undoable. If the
command is undoable, each executed instance of that command is added to the
undo queue. '''
# We must return True to specify that this command is undoable.
return True
##########################################################
# Plug-in initialization.
##########################################################
def cmdCreator():
''' Create an instance of our command. '''
return MyUndoableCommand()
def initializePlugin( mobject ):
''' Initialize the plug-in when Maya loads it. '''
mplugin = OpenMaya.MFnPlugin( mobject )
try:
mplugin.registerCommand( kPluginCmdName, cmdCreator )
except:
sys.stderr.write( 'Failed to register command: ' + kPluginCmdName )
def uninitializePlugin( mobject ):
''' Uninitialize the plug-in when Maya un-loads it. '''
mplugin = OpenMaya.MFnPlugin( mobject )
try:
mplugin.deregisterCommand( kPluginCmdName )
except:
sys.stderr.write( 'Failed to unregister command: ' + kPluginCmdName )
##########################################################
# Sample usage.
##########################################################
'''
# Copy the following lines and run them in Maya's Python Script Editor:
import maya.cmds as cmds
cmds.loadPlugin( 'pySampleUndoableCommand.py' )
cmds.myUndoableCommandName()
'''
Python API 1.0:
# sampleUndoableCommand.py
import sys
import maya.OpenMayaMPx as OpenMayaMPx
# ... additional imports here ...
kPluginCmdName = 'myUndoableCommandName'
##########################################################
# Plug-in
##########################################################
class MyUndoableCommand( OpenMayaMPx.MPxCommand ):
def __init__(self):
''' Constructor. '''
OpenMayaMPx.MPxCommand.__init__(self)
def doIt(self, args):
''' doIt() is called once when the command is first executed. '''
# ... Perform any object creation here, since doIt() is only called once per command instance ...
# Call self.redoIt() to perform the command's actual work. This function call flow
# is useful for code re-use.
self.redoIt()
def redoIt(self):
''' redoIt() is called every time the instance of this command is re-done from
the undo queue. '''
# ... Perform any object manipulation here ...
pass
def undoIt(self):
''' undoIt() is called every time the instance of this command is undone. '''
# ... Reverse any object creation or manipulations here ...
pass
def isUndoable(self):
''' This function indicates whether or not the command is undoable. If the
command is undoable, each executed instance of that command is added to the
undo queue. '''
# We must return True to specify that this command is undoable.
return True
##########################################################
# Plug-in initialization.
##########################################################
def cmdCreator():
''' Create an instance of our command. '''
return OpenMayaMPx.asMPxPtr( MyUndoableCommand() )
def initializePlugin( mobject ):
''' Initialize the plug-in when Maya loads it. '''
mplugin = OpenMayaMPx.MFnPlugin( mobject )
try:
mplugin.registerCommand( kPluginCmdName, cmdCreator )
except:
sys.stderr.write( 'Failed to register command: ' + kPluginCmdName )
def uninitializePlugin( mobject ):
''' Uninitialize the plug-in when Maya un-loads it. '''
mplugin = OpenMayaMPx.MFnPlugin( mobject )
try:
mplugin.deregisterCommand( kPluginCmdName )
except:
sys.stderr.write( 'Failed to unregister command: ' + kPluginCmdName )
##########################################################
# Sample usage.
##########################################################
'''
# Copy the following lines and run them in Maya's Python Script Editor:
import maya.cmds as cmds
cmds.loadPlugin( 'sampleUndoableCommand.py' )
cmds.myUndoableCommandName()
'''
The command plug-in above contains four important functions which are called by Maya during the command execution process:
isUndoable() - If the command's isUndoable()
function returns True
, the instance of that command will be added to Maya's undo stack.
def isUndoable(self):
''' This function indicates whether or not the command is undoable. If the
command is undoable, each executed instance of that command is added to the
undo queue. '''
# We must return True to specify that this command is undoable.
return True
doIt() - The doIt()
function of each command instance is invoked once when the command is first executed. To maximize code re-use, it is recommended to have the doIt()
function instantiate all the objects and variables your command instance will require, and to subsequently call redoIt()
from within doIt()
function body.
def doIt(self, args):
''' doIt() is called once when the command is first executed. '''
# Parse any arguments when doIt() is invoked.
self.parseArgs( args )
# ... Perform any object creation here, since doIt() is only called once per command instance ...
# Call self.redoIt() to perform the command's actual work. This function call flow
# is useful for code re-use.
self.redoIt()
redoIt() - The redoIt()
function is invoked by Maya every time the command is re-done. redoIt()
should therefore be used manipulate the state of the objects used in your command. Keep in mind that this state manipulation should be reversed in the undoIt()
function.
def redoIt(self):
''' redoIt() is called every time the instance of this command is re-done from
the undo queue. '''
# ... Perform any object manipulation here ...
pass
undoIt() - The undoIt()
function is called when the command is undone. The objective of the undoIt()
function is to ensure that each object manipulated by your command has its state reversed to before your command was executed. If the instance of the command does not reverse its manipulations correctly, Maya's internal state will be inconsistent in regards to its undo/redo stack.
def undoIt(self):
''' undoIt() is called every time the instance of this command is undone. '''
# ... Reverse any object creation or manipulations here ...
pass
To facilitate the creation of DAG nodes, and the undoing of DAG node manipulations, the Maya API provides the MDagModifier
class. This class is derived from MDGModifier
, which is used to create and manipulate Dependency Graph nodes. Recall that the DAG is a subset of the Dependency Graph, and so the functions provided by MDGModifier
are also provided by MDagModifier
- this may help you when consulting the class documentation of the function calls made to MDagModifier
in this topic.
In the undoable command that follows, we create and manipulate a camera, and also set the current view to this newly created camera:
Python API 2.0:
# pySampleCameraCommand.py
import sys
import maya.api.OpenMaya as OpenMaya
import maya.api.OpenMayaUI as OpenMayaUI
def maya_useNewAPI():
"""
The presence of this function tells Maya that the plugin produces, and
expects to be passed, objects created using the Maya Python API 2.0.
"""
pass
kPluginCmdName = 'myCameraCommand'
##########################################################
# Plug-in
##########################################################
class MyCameraCommand( OpenMaya.MPxCommand ):
def __init__(self):
''' Constructor. '''
OpenMaya.MPxCommand.__init__(self)
def doIt(self, args):
''' doIt() is called once when the command is first executed. '''
# This MDagModifier object will allow us to undo and redo the creation of DAG nodes in our command.
self.dagModifier = OpenMaya.MDagModifier()
# Create the camera transform node.
self.cameraTransformObj = self.dagModifier.createNode( 'transform' )
self.dagModifier.renameNode( self.cameraTransformObj, 'myCameraTransform' )
# Create the camera shape node as a child of the camera transform node.
self.cameraShapeObj = self.dagModifier.createNode( 'camera', self.cameraTransformObj )
self.dagModifier.renameNode( self.cameraShapeObj, 'myCameraShape' )
# Call self.redoIt() to perform the command's actual work. This function call flow
# is useful for code re-use.
self.redoIt()
def redoIt(self):
''' redoIt() is called every time the instance of this command is re-done from
the undo queue. '''
# Perform the operations enqueued within our reference to MDagModifier. This effectively
# creates the DAG nodes specified using self.dagModifier.createNode().
self.dagModifier.doIt()
# Set the translation value of the camera's transform node.
transformFn = OpenMaya.MFnTransform( self.cameraTransformObj )
transformFn.setTranslation( OpenMaya.MVector( 0, 5, 30 ), OpenMaya.MSpace.kTransform )
# Store the previous camera before we switch to the camera created within this command.
# In undo() we will revert to this previous camera.
self.previousCamera = OpenMaya.MDagPath()
currentView = OpenMayaUI.M3dView.active3dView()
self.previousCamera=currentView.getCamera() # self.previousCamera is now populated with the current camera before we switch.
# Get the DAG path of our camera shape node.
cameraDagPath = OpenMaya.MDagPath()
dagNodeFn = OpenMaya.MFnDagNode( self.cameraShapeObj )
cameraDagPath = dagNodeFn.getPath()
# Set the camera view to the one we switched
currentView.setCamera( cameraDagPath )
def undoIt(self):
''' undoIt() is called every time the instance of this command is undone. '''
# Switch back to the previous camera. We do not have to reverse the translation of
# self.cameraTransformObj because it will be excluded from the DAG once
# self.dagModifier.undoIt() is called below.
currentView = OpenMayaUI.M3dView.active3dView()
self.previousCamera = currentView.getCamera()
# This call to MDagModifier.undoIt() undoes all the operations within the MDagModifier.
self.dagModifier.undoIt()
def isUndoable(self):
''' This function indicates whether or not the command is undoable. If the
command is undoable, each executed instance of that command is added to the
undo queue. '''
# We must return True to specify that this command is undoable.
return True
##########################################################
# Plug-in initialization.
##########################################################
def cmdCreator():
''' Create an instance of our command. '''
return MyCameraCommand()
def initializePlugin( mobject ):
''' Initialize the plug-in when Maya loads it. '''
mplugin = OpenMaya.MFnPlugin( mobject )
try:
mplugin.registerCommand( kPluginCmdName, cmdCreator )
except:
sys.stderr.write( 'Failed to register command: ' + kPluginCmdName )
def uninitializePlugin( mobject ):
''' Uninitialize the plug-in when Maya un-loads it. '''
mplugin = OpenMaya.MFnPlugin( mobject )
try:
mplugin.deregisterCommand( kPluginCmdName )
except:
sys.stderr.write( 'Failed to unregister command: ' + kPluginCmdName )
##########################################################
# Sample usage.
##########################################################
'''
# Copy the following lines and run them in Maya's Python Script Editor:
import maya.cmds as cmds
cmds.loadPlugin( 'pySampleCameraCommand.py' )
cmds.myCameraCommand()
cmds.undo()
cmds.redo()
'''
Python API 1.0:
# sampleCameraCommand.py
import sys
import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMaya as OpenMaya
import maya.OpenMayaUI as OpenMayaUI
kPluginCmdName = 'myCameraCommand'
##########################################################
# Plug-in
##########################################################
class MyCameraCommand( OpenMayaMPx.MPxCommand ):
def __init__(self):
''' Constructor. '''
OpenMayaMPx.MPxCommand.__init__(self)
def doIt(self, args):
''' doIt() is called once when the command is first executed. '''
# This MDagModifier object will allow us to undo and redo the creation of DAG nodes in our command.
self.dagModifier = OpenMaya.MDagModifier()
# Create the camera transform node.
self.cameraTransformObj = self.dagModifier.createNode( 'transform' )
self.dagModifier.renameNode( self.cameraTransformObj, 'myCameraTransform' )
# Create the camera shape node as a child of the camera transform node.
self.cameraShapeObj = self.dagModifier.createNode( 'camera', self.cameraTransformObj )
self.dagModifier.renameNode( self.cameraShapeObj, 'myCameraShape' )
# Call self.redoIt() to perform the command's actual work. This function call flow
# is useful for code re-use.
self.redoIt()
def redoIt(self):
''' redoIt() is called every time the instance of this command is re-done from
the undo queue. '''
# Perform the operations enqueued within our reference to MDagModifier. This effectively
# creates the DAG nodes specified using self.dagModifier.createNode().
self.dagModifier.doIt()
# Set the translation value of the camera's transform node.
transformFn = OpenMaya.MFnTransform( self.cameraTransformObj )
transformFn.setTranslation( OpenMaya.MVector( 0, 5, 30 ), OpenMaya.MSpace.kTransform )
# Store the previous camera before we switch to the camera created within this command.
# In undo() we will revert to this previous camera.
self.previousCamera = OpenMaya.MDagPath()
currentView = OpenMayaUI.M3dView.active3dView()
currentView.getCamera( self.previousCamera ) # self.previousCamera is now populated with the current camera before we switch.
# Get the DAG path of our camera shape node.
cameraDagPath = OpenMaya.MDagPath()
dagNodeFn = OpenMaya.MFnDagNode( self.cameraShapeObj )
dagNodeFn.getPath( cameraDagPath )
# Set the camera view to the one we switched
currentView.setCamera( cameraDagPath )
def undoIt(self):
''' undoIt() is called every time the instance of this command is undone. '''
# Switch back to the previous camera. We do not have to reverse the translation of
# self.cameraTransformObj because it will be excluded from the DAG once
# self.dagModifier.undoIt() is called below.
currentView = OpenMayaUI.M3dView.active3dView()
currentView.setCamera( self.previousCamera )
# This call to MDagModifier.undoIt() undoes all the operations within the MDagModifier.
self.dagModifier.undoIt()
def isUndoable(self):
''' This function indicates whether or not the command is undoable. If the
command is undoable, each executed instance of that command is added to the
undo queue. '''
# We must return True to specify that this command is undoable.
return True
##########################################################
# Plug-in initialization.
##########################################################
def cmdCreator():
''' Create an instance of our command. '''
return OpenMayaMPx.asMPxPtr( MyCameraCommand() )
def initializePlugin( mobject ):
''' Initialize the plug-in when Maya loads it. '''
mplugin = OpenMayaMPx.MFnPlugin( mobject )
try:
mplugin.registerCommand( kPluginCmdName, cmdCreator )
except:
sys.stderr.write( 'Failed to register command: ' + kPluginCmdName )
def uninitializePlugin( mobject ):
''' Uninitialize the plug-in when Maya un-loads it. '''
mplugin = OpenMayaMPx.MFnPlugin( mobject )
try:
mplugin.deregisterCommand( kPluginCmdName )
except:
sys.stderr.write( 'Failed to unregister command: ' + kPluginCmdName )
##########################################################
# Sample usage.
##########################################################
'''
# Copy the following lines and run them in Maya's Python Script Editor:
import maya.cmds as cmds
cmds.loadPlugin( 'sampleCameraCommand.py' )
cmds.myCameraCommand()
cmds.undo()
cmds.redo()
'''
In the sample above, the key functions we are interested in (for now) are: MDagModifer.createNode()
, MDagModifier.doIt()
and MDagModifier.undoIt()
. These functions are respectively distributed among our command's doIt()
, redoIt()
and undoIt()
methods:
MDagModifier.createNode() - When MDagModifier.createNode()
is called, the operation is queued within the MDagModifier
, and an instance of MObject
is returned. This MObject
is a handle to a newly created Maya DAG node which has not yet been included into DAG. Observe that we are invoking the method signature of MDagModifier.createNode()
which accepts a string as the first parameter. This string represents the created node type's name. Valid node type names are listed under Maya User Guide > Technical Documentation > Nodes, for example: 'transform'
and 'camera'
. The call to MDagModifier.renameNode()
is also queued within the MDagModifier
, and is used to do/undo the renaming of a node.
def doIt(self, args):
''' doIt() is called once when the command is first executed. '''
# This MDagModifier object will allow us to undo and redo the creation of DAG nodes in our command.
self.dagModifier = OpenMaya.MDagModifier()
# Create the camera transform node.
self.cameraTransformObj = self.dagModifier.createNode( 'transform' )
self.dagModifier.renameNode( self.cameraTransformObj, 'myCameraTransform' )
# Create the camera shape node as a child of the camera transform node.
self.cameraShapeObj = self.dagModifier.createNode( 'camera', self.cameraTransformObj )
self.dagModifier.renameNode( self.cameraShapeObj, 'myCameraShape' )
# Call self.redoIt() to perform the command's actual work. This function call flow
# is useful for code re-use.
self.redoIt()
MDagModifier.doIt() - When MDagModifier.doIt()
is called, the previously queued operations are effectively executed. As such, the queued MDagModifier.createNode()
operation will complete by adding the created node into the DAG. The call to MDagModifier.doIt()
in our command's redoIt()
method effectively lets us operate on the same set of created nodes defined in our command's doIt()
method. Observe that we are not creating new nodes repeatedly when our command's redoIt()
method is invoked.
def redoIt(self):
''' redoIt() is called every time the instance of this command is re-done from
the undo queue. '''
# Perform the operations enqueued within our reference to MDagModifier.
self.dagModifier.doIt()
# ... object manipulations ...
MDagModifier.doIt()
can be called several times in the same command. This is necessary when a node must be disconnected and deleted. Consult the MDGModifier
class documentation for more information.
MDagModifier.undoIt() - When MDagModifier.undoIt()
is called, all the queued operations which have been completed in calls to MDagModifier.doIt()
are undone.
def undoIt(self):
''' undoIt() is called every time the instance of this command is undone. '''
# ... revert object manipulations ...
# This call to MDagModifier.undoIt() undoes all the operations queued within the MDagModifier.
self.dagModifier.undoIt()
It is sufficient to call MDagModifier.undoIt()
only once, despite the number of calls to MDagModifier.doIt()
within the command.
In our command's redoIt()
method body, we change the position of our camera's transform node via a call to MFnCamera.setTranslation()
:
def redoIt(self):
''' redoIt() is called every time the instance of this command is re-done from
the undo queue. '''
# ...
# Set the translation value of the camera's transform node.
transformFn = OpenMaya.MFnTransform( self.cameraTransformObj )
transformFn.setTranslation( OpenMaya.MVector( 0, 5, 30 ), OpenMaya.MSpace.kTransform )
# ...
This introduces the use of MObject
s and function sets in the Maya API.
Maya Objects (MObject
) - The Maya API provides access to Maya objects as instances of the MObject
wrapper class. Instances of MObject
can therefore represent cameras, lights, meshes, transforms, and dependency graph nodes. The internal state of an MObject
is manipulated via a compatible function set (classes derived from MFnBase
). All instances of MObject
have one and only one Maya Object type (MObject.apiType()
), for example: MFn.kTransform
, or MFn.kCamera
, which may be compatible with multiple types of function sets. The MObject.hasFn()
method determines if an instance of MObject
is compatible with a given function set.
Function Sets (MFnBase
and subclasses) - Function sets are provided in the Maya API to manipulate the state of compatible MObject
s. They exist as singletons, meaning that Maya contains only one function set of each type internally. A function set can only manipulate one MObject
at a time, though this object can be changed using MFnBase.setObject()
. A reference to a specific function set is obtained via its constructor, which can accept an MObject
to set the current working object.
An MObject
's function set compatibility follows the class hierarchy up to the MFnBase
class. An instance of MObject
with a Maya Object type of MFn.kCamera
is therefore compatible with the following function sets: MFnCamera
, MFnDagNode
, MFnDependencyNode
, and MFnBase
The code sample and diagram below illustrate the use of the MFnTransform
function set to set the translation of a compatible MObject
(of type Mfn.kTransform
), namely the transform node we had previously assigned to self.cameraTransformObj
and named as 'myCameraTransform'
.
# Set the translation value of the camera's transform node.
transformFn = OpenMaya.MFnTransform( self.cameraTransformObj )
transformFn.setTranslation( OpenMaya.MVector( 0, 5, 30 ), OpenMaya.MSpace.kTransform )
The second parameter (OpenMaya.MSpace.kTransform
) in the call to MFnTransform.setTranslation()
specifies that the translation should be applied in the transform node's local coordinate space.
You will notice that the majority of function sets also provide a create()
function, for example: MFnCamera.create()
, which takes care of instantiating an appropriate shape and transform node for you. We recommend only using these functions within one-time-use, non-undoable commands, for example to initialize a scene. An exception to this guideline is MFnMesh.create()
, whose use in a command is illustrated in Example: Creating a Scene. In this example, we create a parent transform node with MDagModifier.createNode()
, and must use MFnMesh.create()
to define the mesh data under it as a child node.
In a similar fashion, you may notice that the MFnDagNode
function set contains MFnDagNode.addChild()
, MFnDagNode.removeChild()
, and other DAG hierarchy manipulation functions. Unless you are creating a single-use, non-undoable command, or want to manually implement your command's undoIt()
logic to reverse these actions, it is preferable to use the MDagModifier.reparentNode()
function, followed by a call to MDagModifier.doIt()
.
You may also be tempted to reverse your command's node creations via calls to MGlobal.deleteNode()
within your command's undoIt()
method. Doing so is not advisable, as calling MGlobal.deleteNode()
actually executes the MEL node deletion command internally, thereby causing Maya to add a command to the undo stack while it is in the process of undoing a command. This causes an inconsistency within Maya's undo stack, and can result in a crash.
For the same reason, we do not recommend invoking maya.cmds
functions within a command plug-in. The safe alternative is to use the MDagModifier.commandToExecute()
function to enqueue MEL commands which will be safely executed when MDagModifier.doIt()
is called. The same MEL commands will be safely undone when MDagModifier.undoIt()
is called, thus avoiding any undo stack inconsistencies within Maya. Consult Example: Creating an IK Joint Chain for more information.