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.
# 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() '''
# 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:
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
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()
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
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:
# 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() '''
# 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:
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. self.dagModifier.doIt() # ... object manipulations ...
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()
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 MObjects 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 MObjects. 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 )
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.