[docs]defsetJournalToggle(state):"""By default we don't keep journal for all the creation or edit by omx. Use this function to turn it on. Notes: Keep in mind this is a global state, turning on journal will slow down the overall performance! Another way to toggle the journal on is to set it to None (default value) and set the logging level to ``logging.DEBUG`` for ``AL.omx._xmodifier``. Also turning the journal on only makes omx start to record journal, the creation or edits that are already done still won't be in the journal. Args: state (bool | None): the state of toggle. True = force on, off = force off, None = depends on it is DEBUG logging level """global_JOURNAL_TOGGLE_JOURNAL_TOGGLE=stateifstateisNone:logger.info("The omx journal state now depends on logging level for AL.omx._xmodifier")else:logger.info("The omx journal state is turned %s",state)
[docs]defisJournalOn():"""Query if we are actually recording journal for each creation or edit by omx. """if_JOURNAL_TOGGLEisNone:returnlogger.isEnabledFor(logging.DEBUG)return_JOURNAL_TOGGLE
[docs]classJournalContext:"""A python context where you set the journal state by force. Notes: Turning on by calling :func:`setJournalToggle(True)` will slowdown omx performance globally. This is the suggested way to have the journal temporarily set to on/off. """
classNodeCreationLog:"""Helper class to enable/disable and manage tracked nodes, you are suppose to only have one instance of this class in memory, within this python module. Notes: Log entries are done in a First In Last Out approach to allow for nested tracking when a parent that wants to track nodes calls a child that also wants to track nodes. The data is held in this form: [ [:class:`om2.MObjectHandle`,...], [:class:`om2.MObjectHandle`,...], ... ] """def__init__(self):self._log=[]self._isActive=FalsedefbeginNewLog(self):"""Adds a new list to the node creation log. """self._log.append([])self._isActive=TruedefclearLogEntry(self,clearAll=False):"""Remove all or the last list of tracked nodes in the log. Args: clearAll (bool, optional): If true, remove the entire log. """ifclearAll:self._log=[]else:self._log.pop()self._isActive=len(self._log)>=1deftrackedNodes(self,queryAll=False):"""The nodes that have been tracked in the creation log. Args: queryAll (bool, optional): If true, get the entire log, otherwise retrieve the last key in the log. Returns: list[:class:`om2.MObjectHandle`]: The list of created nodes. """ifnotself._log:logger.warning("No tracked nodes in the creation log.")return[]ifqueryAll:return[nodefortrackedNodesinself._logfornodeintrackedNodes]returnself._log[-1]deftrackNode(self,node):"""Add a node to the last active key in the tracking log. Args: node (:class:`XNode`): The node to track. """ifnotself.isActive():returnmobHandle=object.__getattribute__(node,"_mobHandle")self._log[-1].append(mobHandle)defisActive(self):"""Check if the log is currently in use. Returns: bool: The active status of the log. """returnself._isActive_NODE_CREATION_LOG=NodeCreationLog()defstartTrackingNodes():"""Start a new entry in the node creation log to track any nodes created with createNode/createDagNode/createDGNode calls. """_NODE_CREATION_LOG.beginNewLog()defendTrackingNodes(endAll=False):"""Stop and clear the last (or all) active log(s) of tracked nodes. Args: endAll (bool, optional): If true, ends all active tracking. Returns: list[:class:`om2.MObjectHandle`]: The list of created nodes that had been tracked. """ifnot_NODE_CREATION_LOG.isActive():logger.debug("No active omx._xmodifier creation log to end.")return[]createdNodes=_NODE_CREATION_LOG.trackedNodes(endAll)_NODE_CREATION_LOG.clearLogEntry(endAll)returncreatedNodes
[docs]defqueryTrackedNodes(queryAll=False):"""The mobject handles to the nodes that have been created since tracking has been started. Args: queryAll (bool, optional): If true, return the entire list of handles, otherwise just the handles since startTrackingNodes has last been called. Returns: list[:class:`om2.MObjectHandle`]: The list of created nodes. """ifnot_NODE_CREATION_LOG.isActive():logger.info("No active creation log to query.")return[]return_NODE_CREATION_LOG.trackedNodes(queryAll)
[docs]classTrackCreatedNodes:"""A Python Context Decorator to temporarily track nodes that have been created with omx Examples: .. code:: python # The class can be used as decorator, or python context: @TrackCreatedNodes() def methodToCreateNodes(): # Create nodes nodesCreated = omx.queryTrackedNodes() def methodToCreateNodes(): with TrackCreatedNodes() as tracker: # Create nodes nodesCreated = tracker.trackedNodes() """def__call__(self,func):@wraps(func)defwrapper(*args,**kw):withself:returnfunc(*args,**kw)returnwrapperdef__enter__(self):startTrackingNodes()returnselfdef__exit__(self,*_,**__):endTrackingNodes()
[docs]deftrackedNodes(self,queryAll=False):"""Get the om2.MObjectHandle(s) created that are tracked. Args: queryAll (bool, optional): Whether return all batches of om2.MObjectHandles or just the last batch. Returns: [:class:`om2.MObjectHandle`]: Created nodes. """returnqueryTrackedNodes(queryAll)
classXModifierLog:__slots__=["method","values"]def__init__(self,method,values):"""Internal wrapper object to hold the method name and argument values. Args: method (str): the method name. values (list): the list of arguments for method call, excluding self. """self.method=methodself.values=valuesdef__str__(self):returnf"{self.method}({self.values})"def_modifierMethod(method):"""A function decorator for :class:`XModifier` methods. Notes: This decorator; - Converts :class:`XNode` instances to om2.MObjects . - Records a method call log in the journal. - Calls the method. - Calls doIt() to apply the potential edits in immediate mode. Args: method (callable): the callable method object. Returns: callable: the wrapped method """@wraps(method)defwrapper(*args,**kwargs):self=args[0]# Add journal entry if needed, convert all MObjects to MObjectHandlesifisJournalOn():values=inspect.getcallargs(method,*args,**kwargs)delvalues["self"]fork,vinvalues.items():ifisinstance(v,om2.MObject):values[k]=om2.MObjectHandle(v)self._journal.append(XModifierLog(method.__name__,values))# NOQAself._clean=False# NOQA# Process args to convert any XNode to MObjectnewArgs=[]forarginargs:ifisinstance(arg,_xnode.XNode):arg=arg.object()newArgs.append(arg)fork,vinkwargs.items():ifisinstance(v,_xnode.XNode):kwargs[k]=v.object()logger.debug("Calling %s(%s, %s)",method,newArgs,kwargs)res=method(*newArgs,**kwargs)ifself._immediate:# NOQAself.doIt()returnresreturnwrapper
[docs]classXModifier:""" A wrapper around :class:`MModifier` that supports :class:`XNode` instances directly Notes: When created in immediate mode, every time any modifier method is run on this object the doIt method is also run from within a dynamic :class:`AL_OMXCommand` instance to allow undoing. Immediate mode will always be much slower than non-immediate mode, and is only there to allow simple experiments from the Maya script editor. """
[docs]def__init__(self,immediate=False):"""Creates a new XModifier instance. Args: immediate (bool, optional): Specifies if this XModifier should behave in immediate mode. Defaults to False. """self._immediate=immediateself._reset()
[docs]defjournal(self):"""Returns the current list of operations to run. Returns: list(str): A list of strings describing the operations to run. """journal=[]forrecordinself._journal:values={}fork,vinrecord.values.items():ifisinstance(v,om2.MObjectHandle):mob=v.object()ifmob==om2.MObject.kNullObj:values[k]=Noneelifmob.hasFn(om2.MFn.kDependencyNode):values[k]=str(_xnode.XNode(mob))else:values[k]='MObject("unknown")'else:values[k]=str(v)valuesStr=", ".join([f"{repr(k)}: {repr(v)}"fork,vinsorted(values.items())])journal.append(f"mod.{record.method}({{{valuesStr}}})")returnjournal
def_reallyDoIt(self,keepJournal=False):logger.debug("%r._reallyDoIt() called",self)try:ifself._immediate:# this will call the self._modifier.doIt() method directly!cmds.AL_OMXCommand()else:DoItModifierWrapper(self,self._modifier).doIt()finally:iflogger.isEnabledFor(logging.DEBUG):ifisJournalOn():logger.debug("Just called doIt on:\n%s","\n".join(self.journal()))else:logger.debug("Just called doIt (No journal available)")ifnotkeepJournal:self._journal=[]
[docs]defisClean(self):"""Returns True if the modifier has nothing to do. Notes: It will also return True if the modifier has already been used by a Command. Returns: bool: the clean state. """returnself._clean
[docs]defdoIt(self,keepJournal=False):"""Executes the operations held by this modifier in Maya. Notes: In immediate mode this will actually execute doIt from within a dynamic Maya command to allow undo to function. If doIt() is called multiple times in a row, without any intervening calls to undoIt(), then only the operations which were added since the previous doIt() call will be executed. If undoIt() has been called then the next call to doIt() will do all operations. Args: keepJournal (bool, optional): Retains the journal for further inspection. Defaults to False. """logger.debug("%r.doIt() called",self)ifself._immediateandselfnotin_CURRENT_MODIFIER_LIST:# This can happen if something is keeping an instance of an XModifier and calling doIt in immediate mode..._CURRENT_MODIFIER_LIST.append(self)self._reallyDoIt(keepJournal=keepJournal)ifself._immediate:# Create a new modifier to ensure modifiers are not shared across command instancesself._reset()
[docs]defundoIt(self,keepJournal=False):"""Undo the modifier operation in Maya. In immediate mode this function does nothing, as you should already be able to undo it in Maya. Notes: It is only used in the scenario that a user creates a modifier manually by calling omx.newModifier() Args: keepJournal (bool, optional): Retains the journal for further inspection. Defaults to False. """logger.debug("%r.undoIt() called",self)try:ifself._immediate:# Immediate mode hands undo/redo controls to Maya:warnings.warn("In the immediate mode, Maya takes care of the undo/redo.",RuntimeWarning,)else:DoItModifierWrapper(self,self._modifier).undoIt()finally:iflogger.isEnabledFor(logging.DEBUG):ifisJournalOn():logger.debug("Just called undoIt on:\n%s","\n".join(self.journal()))else:logger.debug("Just called undoIt (No journal available)")ifnotkeepJournal:self._journal=[]
[docs]@_modifierMethoddefaddAttribute(self,node,attribute):"""Adds an attribute to a node. Adds an operation to the modifier to add a new dynamic attribute to the given dependency node. If the attribute is a compound its children will be added as well, so only the parent needs to be added using this method. Args: node (:class:`XNode` | :class:`om2.MObject`): the node to add an attribute to attribute (:class:`om2.MObject`): the attribute MObject Returns: :class:`XModifier`: A reference to self """ifisinstance(node,_xnode.XNode):node=node.object()self._modifier.addAttribute(node,attribute)returnself
[docs]@_modifierMethoddefaddExtensionAttribute(self,nodeClass,attribute):"""Adds an extension attribute to a node class Notes: Adds an operation to the modifier to add a new extension attribute to the given node class. If the attribute is a compound its children will be added as well, so only the parent needs to be added using this method. Args: nodeClass (:class:`om2.MNodeClass`): The node class attribute (:class:`om2.MObject`): The attribute MObject to add Returns: :class:`XModifier`: A reference to self """self._modifier.addExtensionAttribute(nodeClass,attribute)returnself
[docs]@_modifierMethoddefcommandToExecute(self,command):"""Adds an operation to the modifier to execute a MEL command. Notes: The command should be fully undoable otherwise unexpected results may occur. If the command contains no undoable portions whatsoever, the call to doIt() may fail, but only after executing the command. It is best to use multiple commandToExecute() calls rather than batching multiple commands into a single call to commandToExecute(). They will still be undone together, as a single undo action by the user, but Maya will better be able to recover if one of the commands fails. Args: command (str): The command string Returns: :class:`XModifier`: A reference to self """self._modifier.commandToExecute(command)returnself
[docs]@_modifierMethoddefconnect(self,*args,**kwargs):"""Connects two plugs. Adds an operation to the modifier that connects two plugs in the dependency graph. It is the user's responsibility to ensure that the source and destination attributes are of compatible types. For instance, if the source attribute is a nurbs surface then the destination must also be a nurbs surface. Plugs can either be specified with node and attribute MObjects or with MPlugs. Note: Arguments v1: If any plug is an array plug, it will expect another is also an array plug with matched data type, and it will try to connect at array level: source (:class:`XPlug` | :class:`om2.MPlug`): The source plug dest (:class:`XPlug` | :class:`om2.MPlug`): The destination plug Arguments v2: If any plug is an array plug, it will try to connect at the next available element level: sourceNode (:class:`MObject`): The source MObject sourceAttr (:class:`MObject`): The source attribute MObject destNode (:class:`MObject`): The destination MObject destAttr (:class:`MObject`): The destination attribute MObject Returns: :class:`XModifier`: A reference to self """returnself._modifier.connect(*args,**kwargs)
[docs]@_modifierMethoddefcreateDGNode(self,typeName,nodeName=""):"""Creates a DG node. Args: typeName (str): the type of the object to create, e.g. "transform". nodeName (str, optional): the node name, if non empty will be used in a modifier.renameObject call. Defaults to "". Returns: :class:`XNode`: A XNode instance around the created MObject. """mob=self._modifier.createDGNode(typeName)ifnodeName:# Call parent method directly to avoid an entry in the journalself._modifier.renameNode(mob,nodeName)# To get a valid MObjectHandle in XNode the creation needs to happen right awayself._modifier.doIt()node=_xnode.XNode(mob)_NODE_CREATION_LOG.trackNode(node)returnnode
[docs]@_modifierMethoddefcreateDagNode(self,typeName,parent=om2.MObject.kNullObj,nodeName="",manageTransformIfNeeded=True,returnAllCreated=False,):"""Creates a DAG node. Adds an operation to the modifier to create a DAG node of the specified type. If a parent DAG node is provided the new node will be parented under it. If no parent is provided and the new DAG node is a transform type then it will be parented under the world. In both of these cases the method returns the new DAG node. If no parent is provided and the new DAG node is not a transform type then a transform node will be created and the child parented under that. The new transform will be parented under the world and it is the transform node which will be returned by the method, not the child. None of the newly created nodes will be added to the DAG until the modifier's doIt() method is called. Notes: If you try to use :func:`createDagNode()` to create an empty NurbsCurve or Mesh, calling bestFn() on the returned :class:`XNode` will give you `MFnNurbsCurve` or `MFnMesh` but these are invalid to work with. You will end up getting a misleading "Object does not exist." error as Maya doesn't like an empty NurbsCurve or Mesh. Raises: :class:`TypeError` if the node type does not exist or if the parent is not a transform type. Args: typeName (str): the type of the object to create, e.g. "transform". parent (:class:`om2.MObject` | :class:`XNode`, optional): An optional parent for the DAG node to create. nodeName (str, optional): the node name, if non empty will be used in a modifier.renameObject call. Defaults to "". manageTransformIfNeeded (bool, optional): when you create a shape without a parent, Maya will create both transform and shape, and return parent om2.MObject instead. if manageTransformIfNeeded is True, than we will also rename the transform, and return shape MObject instead. Most of time we keep it default True value. returnAllCreated (bool, optional): If True, it will return all newly created nodes, potentially including any new parent transforms and the shape of the type. Returns: :class:`XNode` | list: An _xnode.XNode instance around the created MObject, or the list of all created nodes, if returnAllCreated is True. """ifparentisNone:parent=om2.MObject.kNullObjifparent!=om2.MObject.kNullObj:ifisinstance(parent,om2.MObject):xparent=_xnode.XNode(parent)elifisinstance(parent,_xnode.XNode):xparent=parentelse:xparent=_xnode.XNode(parent)ifnotxparent.object().hasFn(om2.MFn.kTransform):parent=xparent.bestFn().parent(0)mob=self._modifier.createDagNode(typeName,parent=parent)allCreated=[]ifreturnAllCreatedelseNoneifparent==om2.MObject.kNullObjorparentisNone:self._modifier.doIt()trn=mobifmanageTransformIfNeeded:# Special case where Maya automatically creates and returns the parent transform instead of the newly created child.trnFn=om2.MFnDagNode(trn)iftrnFn.childCount():mob=trnFn.child(0)availableName=_nodes.closestAvailableNodeName(typeName+"1")self._modifier.renameNode(trn,availableName)ifreturnAllCreated:allCreated.append(_xnode.XNode(trn))ifnodeName:# Call parent method directly to avoid an entry in the journalself._modifier.renameNode(mob,nodeName)# To get a valid MObjectHandle in XNode the creation needs to happen right awayself._modifier.doIt()node=_xnode.XNode(mob)_NODE_CREATION_LOG.trackNode(node)ifreturnAllCreated:allCreated.append(node)returnallCreatedreturnnode
[docs]@_modifierMethoddefcreateNode(self,typeName,*args,**kwargs):"""Convenience method to be able to use an XModifier when a MDagModifier or MDGModifier is expected. Args: typeName (str): the type of the object to create, e.g. "transform" Returns: :class:`om2.MObject`: The created MObject """# if any parent keyword is specified, we want to create a dag node for sure. Otherwise, we check the node type.ifkwargs.get("parent")or"dagNode"incmds.nodeType(typeName,inherited=True,isTypeName=True):returnself.createDagNode(typeName,*args,**kwargs).object()returnself.createDGNode(typeName,*args,**kwargs).object()
[docs]@_modifierMethoddefdeleteNode(self,node):"""Deletes the node Adds an operation to the modifer which deletes the specified node from the Dependency Graph. If the modifier already contains other operations on the same node (e.g. a disconnect) then they should be committed by calling the modifier's doIt() before the deleteNode operation is added. Args: node (:class:`XNode` | :class:`om2.MObject`): The object to delete. Returns: :class:`XModifier`: A reference to self """self._modifier.deleteNode(node)returnself
[docs]@_modifierMethoddefdisconnect(self,*args,**kwargs):"""Disconnects two plugs Adds an operation to the modifier that breaks a connection between two plugs in the dependency graph. Plugs can either be specified with node and attribute MObjects or with MPlugs. Note: Arguments v1: It works for all the scenarios, including disconnecting two array plugs at array level. source (:class:`XPlug` | :class:`om2.MPlug`): The source plug dest (:class:`XPlug` | :class:`om2.MPlug`): The destination plug Arguments v2: Unlike the connect() version, it does not work on array attributes. sourceNode (:class:`MObject`): The source MObject sourceAttr (:class:`MObject`): The source attribute MObject destNode (:class:`MObject`): The destination MObject destAttr (:class:`MObject`): The destination attribute MObject Returns: :class:`XModifier`: A reference to self """self._modifier.disconnect(*args,**kwargs)returnself
[docs]@_modifierMethoddeflinkExtensionAttributeToPlugin(self,plugin,attribute):""" Links an extension attribute to a plugin The plugin can call this method to indicate that the extension attribute defines part of the plugin, regardless of the node type to which it attaches itself. This requirement is used when the plugin is checked to see if it is in use or if is able to be unloaded or if it is required as part of a stored file. For compound attributes only the topmost parent attribute may be passed in and all of its children will be included, recursively. Thus it's not possible to link a child attribute to a plugin by itself. Note that the link is established immediately and is not affected by the modifier's doIt() or undoIt() methods. Args: plugin (:class:`om2.MObject`): The plugin attribute (:class:`om2.MObject`): The attribute MObject Returns: :class:`XModifier`: A reference to self """self._modifier.linkExtensionAttributeToPlugin(plugin,attribute)returnself
[docs]@_modifierMethoddefnewPlugValue(self,plug,value):"""Sets a new plug value. Adds an operation to the modifier to set the value of a plug, where value is an MObject data wrapper, such as created by the various MFn*Data classes. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (:class:`om2.MObject`): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValue(plug,value)returnself
[docs]@_modifierMethoddefnewPlugValueBool(self,plug,value):"""Adds an operation to the modifier to set a value onto a bool plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (bool): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValueBool(plug,value)returnself
[docs]@_modifierMethoddefnewPlugValueChar(self,plug,value):"""Adds an operation to the modifier to set a value onto a char (single byte signed integer) plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (int): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValueChar(plug,value)returnself
[docs]@_modifierMethoddefnewPlugValueDouble(self,plug,value):"""Adds an operation to the modifier to set a value onto a double-precision float plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (float): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValueDouble(plug,value)returnself
[docs]@_modifierMethoddefnewPlugValueFloat(self,plug,value):"""Adds an operation to the modifier to set a value onto a single-precision float plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (float): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValueFloat(plug,value)returnself
[docs]@_modifierMethoddefnewPlugValueInt(self,plug,value):"""Adds an operation to the modifier to set a value onto an int plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (int): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValueInt(plug,value)returnself
[docs]@_modifierMethoddefnewPlugValueMAngle(self,plug,value):"""Adds an operation to the modifier to set a value onto an angle plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (:class:`om2.MAngle`): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValueMAngle(plug,value)returnself
[docs]@_modifierMethoddefnewPlugValueMDistance(self,plug,value):"""Adds an operation to the modifier to set a value onto a distance plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (:class:`om2.MDistance`): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValueMDistance(plug,value)returnself
[docs]@_modifierMethoddefnewPlugValueMTime(self,plug,value):"""Adds an operation to the modifier to set a value onto a time plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (:class:`om2.MTime`): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValueMTime(plug,value)returnself
[docs]@_modifierMethoddefnewPlugValueShort(self,plug,value):"""Adds an operation to the modifier to set a value onto a short integer plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (int): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValueShort(plug,value)returnself
[docs]@_modifierMethoddefnewPlugValueString(self,plug,value):"""Adds an operation to the modifier to set a value onto a string plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug value (str): The value Returns: :class:`XModifier`: A reference to self """self._modifier.newPlugValueString(plug,value)returnself
[docs]@_modifierMethoddefpythonCommandToExecute(self,callable_):""" Adds an operation to execute a python command Adds an operation to the modifier to execute a Python command, which can be passed as either a Python callable or a string containing the text of the Python code to be executed. The command should be fully undoable otherwise unexpected results may occur. If the command contains no undoable portions whatsoever, the call to doIt() may fail, but only after executing the command. It is best to use multiple calls rather than batching multiple commands into a single call to pythonCommandToExecute(). They will still be undone together, as a single undo action by the user, but Maya will better be able to recover if one of the commands fails. Args: callable (callable | str): The command to execute Returns: :class:`XModifier`: A reference to self """self._modifier.pythonCommandToExecute(callable_)returnself
[docs]@_modifierMethoddefremoveAttribute(self,node,attribute):"""Removes a dynamic attribute. Adds an operation to the modifier to remove a dynamic attribute from the given dependency node. If the attribute is a compound its children will be removed as well, so only the parent needs to be removed using this method. The attribute MObject passed in will be set to kNullObj. There should be no function sets attached to the attribute at the time of the call as their behaviour may become unpredictable. Args: node (:class:`XNode` | :class:`om2.MObject`): the node to remove the attribute from attribute (:class:`om2.MObject`): the attribute MObject Returns: :class:`XModifier`: A reference to self """self._modifier.removeAttribute(node,attribute)returnself
[docs]@_modifierMethoddefremoveExtensionAttribute(self,nodeClass,attribute):"""Removes an extension attribute. Adds an operation to the modifier to remove an extension attribute from the given node class. If the attribute is a compound its children will be removed as well, so only the parent needs to be removed using this method. The attribute MObject passed in will be set to kNullObj. There should be no function sets attached to the attribute at the time of the call as their behaviour may become unpredictable. Args: nodeClass (:class:`om2.MNodeClass`): The node class attribute (:class:`om2.MObject`): The attribute MObject to add Returns: :class:`XModifier`: A reference to self """self._modifier.removeExtensionAttribute(nodeClass,attribute)returnself
[docs]@_modifierMethoddefremoveExtensionAttributeIfUnset(self,nodeClass,attribute):"""Removes an extension attribute. Adds an operation to the modifier to remove an extension attribute from the given node class, but only if there are no nodes in the graph with non-default values for this attribute. If the attribute is a compound its children will be removed as well, so only the parent needs to be removed using this method. The attribute MObject passed in will be set to kNullObj. There should be no function sets attached to the attribute at the time of the call as their behaviour may become unpredictable. Args: nodeClass (:class:`om2.MNodeClass`): The node class attribute (:class:`om2.MObject`): The attribute MObject to add Returns: :class:`XModifier`: A reference to self """self._modifier.removeExtensionAttributeIfUnset(nodeClass,attribute)returnself
[docs]@_modifierMethoddefremoveMultiInstance(self,plug,breakConnections):"""Adds an operation to the modifier to remove an element of a multi (array) plug. Args: plug (:class:`XPlug` | :class:`om2.MPlug`): The plug breakConnections (bool): breaks the connections Returns: :class:`XModifier`: A reference to self """self._modifier.removeMultiInstance(plug,breakConnections)returnself
[docs]@_modifierMethoddefrenameAttribute(self,node,attribute,newShortName,newLongName):"""Adds an operation to the modifer that renames a dynamic attribute on the given dependency node. Args: node (:class:`XNode` | :class:`om2.MObject`): the node to rename the attribute on attribute (:class:`om2.MObject`): the attribute MObject newShortName (str): The new short name newLongName (str): The new long name Returns: :class:`XModifier`: A reference to self """self._modifier.renameAttribute(node,attribute,newShortName,newLongName)returnself
[docs]@_modifierMethoddefrenameNode(self,node,newName):"""Adds an operation to the modifer to rename a node. Args: node (:class:`XNode` | :class:`om2.MObject`): the node to rename newName (str): the new name Returns: :class:`XModifier`: A reference to self """self._modifier.renameNode(node,newName)returnself
[docs]@_modifierMethoddefsetNodeLockState(self,node,newState):"""Adds an operation to the modifier to set the lockState of a node. Args: node (:class:`XNode` | :class:`om2.MObject`): the node to lock newState (bool): the lock state Returns: :class:`XModifier`: A reference to self """self._modifier.setNodeLockState(node,newState)returnself
[docs]@_modifierMethoddefunlinkExtensionAttributeFromPlugin(self,plugin,attribute):"""Unlinks an extension attribute from a plugin. The plugin can call this method to indicate that it no longer requires an extension attribute for its operation. This requirement is used when the plugin is checked to see if it is in use or if is able to be unloaded or if it is required as part of a stored file. For compound attributes only the topmost parent attribute may be passed in and all of its children will be unlinked, recursively. Thus it's not possible to unlink a child attribute from a plugin by itself. Note that the link is broken immediately and is not affected by the modifier's doIt() or undoIt() methods. Args: plugin (:class:`om2.MObject`): The plugin attribute (:class:`om2.MObject`): The attribute MObject to add Returns: :class:`XModifier`: A reference to self """self._modifier.unlinkExtensionAttributeFromPlugin(plugin,attribute)returnself
[docs]@_modifierMethoddefreparentNode(self,node,newParent=None,absolute=False):"""Adds an operation to the modifier to reparent a DAG node under a specified parent. Raises TypeError if the node is not a DAG node or the parent is not a transform type. If no parent is provided then the DAG node will be reparented under the world, so long as it is a transform type. If it is not a transform type then the doIt() will raise a RuntimeError. Args: node (:class:`om2.MObject` | :class:`XNode`): The DAG node to reparent newParent (:class:`om2.MObject` | :class:`XNode`, optional): The new parent. Defaults to None. absolute (bool, optional): Whether or not we try to maintain the world transform of the node. If the node has some transform channels locked, it will try to fill the unlocked channels with debug message. Returns: :class:`XModifier`: A reference to self """ifnotnode.hasFn(om2.MFn.kDagNode):raiseTypeError("The XModifier.reparentNode() received non-DAG node to reparent.")nodeX=_xnode.XNode(node)nodeFn=nodeX.basicFn()ifnewParentisNoneornewParent==om2.MObject.kNullObj:# If asked to reparent to world but it already is:ifnotnodeFn.parentCount():returnselfnewParent=Noneelse:parentNodeX=_xnode.XNode(newParent)# Avoid reparenting to itself:ifnodeX==parentNodeX:raiseRuntimeError("The XModifier.reparentNode() cannot reparent node to itself.")ifnotparentNodeX.hasFn(om2.MFn.kDagNode):raiseTypeError("The XModifier.reparentNode() received non-DAG node to reparent to.")# Avoid reparenting if it is already under the parent:foriinrange(nodeFn.parentCount()):if_xnode.XNode(nodeFn.parent(i))==parentNodeX:returnself# Avoid reparenting parent to a childifnewParent:forpathinom2.MDagPath.getAllPathsTo(newParent):whilepath.length()>0:if_xnode.XNode(path.pop().node())==node:raiseRuntimeError("The XModifier.reparentNode() cannot reparent node to one of its children.")ifnotabsolute:newParent=om2.MObject()ifnotnewParentelsenewParentself._modifier.reparentNode(node,newParent)returnself# We use a cheap version here as the matrix multiplication won't work easily with custom rotate pivot# and scale pivot, plus some corner cases like joint axis...nodePath=om2.MFnDagNode(node).partialPathName()newParentPath=""ifnewParentandom2.MObjectHandle(newParent).isValid():newParentPath=om2.MFnDagNode(newParent).partialPathName()flag="-w"ifnotnewParentPathelse""cmdStr=f"parent -a {flag}{nodePath}{newParentPath}"self._modifier.commandToExecute(cmdStr)returnself
classDoItModifierWrapper:def__init__(self,xmod,mmod):self._xmod=xmodself._mmod=mmoddefdoIt(self):try:self._mmod.doIt()exceptExceptionase:_,exc_value,_=sys.exc_info()ifisJournalOn():j=self._xmod.journal()ifnotj:logger.error("Failed to call doIt: %s",exc_value)iflen(j)==1:logger.error("Failed to call doIt on %s: %s",j[0],exc_value)else:logger.error("Failed to run doIt on operations: %s\n%s",exc_value,"\n".join(j),)journal=", ".join(j)raiseException(f"{exc_value} when calling {journal}")fromeraiseException(f"{exc_value} when calling doIt (journal unavailable)")fromedefundoIt(self):try:self._mmod.undoIt()exceptExceptionase:_,exc_value,_=sys.exc_info()ifisJournalOn():j=self._xmod.journal()ifnotj:logger.error("Failed to call undoIt: %s",exc_value)eliflen(j)==1:logger.error("Failed to call undoIt on %s: %s",j[0],exc_value)else:logger.error("Failed to run undoIt on operations: %s\n%s",exc_value,"\n".join(j),)journal=", ".join(j)raiseException(f"{exc_value} when calling {journal}")fromeraiseException(f"{exc_value} when calling undoIt (journal unavailable)")fromedefredoIt(self):self.doIt()defgetAndClearModifierStack():global_CURRENT_MODIFIER_LISTexistingMods=[]forxmodin_CURRENT_MODIFIER_LIST:ifisinstance(xmod,XModifier):ifxmod.isClean():continuemmod=xmod._modifier# NOQAelse:mmod=Nonelogger.debug("Retrieving mod %r from list for execution",mmod)existingMods.append(DoItModifierWrapper(xmod,mmod))_CURRENT_MODIFIER_LIST=[]returnexistingMods
[docs]defcurrentModifier():"""Returns the last XModifier from the current modifier list. If the current list is empty it creates and returns a new immediate XModifier. Returns: :class:`XModifier`: A :class:`XModifier` instance ready to use. """ifnot_CURRENT_MODIFIER_LIST:mod=XModifier(immediate=True)logger.debug("Added modifier %r to list",mod)_CURRENT_MODIFIER_LIST.append(mod)else:mod=_CURRENT_MODIFIER_LIST[-1]returnmod
[docs]defnewModifier():"""Creates a new non-immediate XModifier, adds it to the current list of modifiers and returns it. Returns: :class:`XModifier`: The newly created XModifier """mod=XModifier(immediate=False)logger.debug("Added modifier %r to list",mod)_CURRENT_MODIFIER_LIST.append(mod)returnmod
[docs]defnewAnimCurveModifier():"""Creates a new `om2anim.MAnimCurveChange` object, adds it to the current list of modifiers and returns it. Returns: :class:`om2anim.MAnimCurveChange`: The newly created MAnimCurveChange """mod=om2anim.MAnimCurveChange()logger.debug("Added modifier %r to list",mod)_CURRENT_MODIFIER_LIST.append(mod)returnmod
defhasCurrentModifier():"""Check if there is any omx modifier ready for an edit. Returns: bool: True if there is, False otherwise. """returnbool(_CURRENT_MODIFIER_LIST)defexecuteModifiersWithUndo():"""Execute modifier actions with undo support. Notes: This will push a ``AL_OMXCommand`` mpx undoable command in the Maya undo queue. """if_CURRENT_MODIFIER_LIST:cmds.AL_OMXCommand()
[docs]@contextlib.contextmanagerdefnewModifierContext():"""Create a new :class:`XModifier` for the context, and call :func:`XModifier.doIt()` on context exit. Notes: Any edits done within the python context, they are using the new :class:`XModifier`. """if_CURRENT_MODIFIER_LIST:# execute any previous doIt upon entering new contextformodin_CURRENT_MODIFIER_LIST:mod.doIt()mod=XModifier(immediate=False)logger.debug("Added modifier %r to list",mod)_CURRENT_MODIFIER_LIST.append(mod)yieldmodmod.doIt()
[docs]@contextlib.contextmanagerdefcommandModifierContext(command):"""A Python Context Manager to be used within ALCommand doIt method. This modifier ensures a non-immediate XModifier is added to the current list of modifiers, and called doIt on exit. Notes: This is a util only for AL internal use. Args: command (:class:`Command`): The command instance """command._managedByXModifer=True# NOQA# For all nested commands, we make sure they are using the same modifier as the outer-most# command, so the undo / redo is linear.nested=_CURRENT_MODIFIER_LISTandgetattr(_CURRENT_MODIFIER_LIST[-1],"_inOperation",False)ifnested:# This command is nested in other commands, it should reuse the modifier to the outer-most# command, which should already have _inOperation state set to True:mod=_CURRENT_MODIFIER_LIST[-1]logger.debug("Possible nested commands found, the outmost XModifier %r will be reused.",mod,)# Call do it before sub command, allowing any exception to raise:mod.doIt()yieldmod# Call do it after sub command, allowing any exception to raise:mod.doIt()else:mod=XModifier(immediate=False)logger.debug("Added modifier %r to list",mod)_CURRENT_MODIFIER_LIST.append(mod)mod._inOperation=True# pylint: disable=protected-accessyieldmodmod._inOperation=False# pylint: disable=protected-accesstry:mod.doIt()finally:command._modifiers=getAndClearModifierStack()# NOQA
[docs]defcreateDagNode(typeName,parent=om2.MObject.kNullObj,nodeName="",returnAllCreated=False):"""Creates a DAG Node within the current active :class:`XModifier` Note: We automatically work around a limitation of the om2.MDagModifier here, where Maya would return the shape's parent transform MObject. Instead we return an :class:`XNode` for the newly created Shape node if the type is of Shape. Args: typeName (str): The type of the DAG node to create. parent (:class:`XNode` | :class:`om2.MObject` | :class:`om2.MFnDagNode` | str, optional): The parent of the DAG node to create. Defaults to `om2.MObject.kNullObj`. nodeName (str, optional): The name of the node to create (used to call mod.renameNode after creation). Defaults to "". returnAllCreated (bool, optional): If True, it will return any newly created nodes, including potential new parent transform and the shape of the type. Returns: :class:`XNode` | [:class:`XNode`]: The created XNode or a list of XNodes, based on returnAllCreated argument. """returncurrentModifier().createDagNode(typeName,parent=parent,nodeName=nodeName,returnAllCreated=returnAllCreated)
[docs]defcreateDGNode(typeName,nodeName=""):"""Creates a DG Node within the current active :class:`XModifier` Args: typeName (str): The node type name. nodeName (str, optional): The node name (to be used in mod.renameNode after creation). Defaults to "". Returns: :class:`XNode` | [:class:`XNode`]: The created XNode. """returncurrentModifier().createDGNode(typeName,nodeName=nodeName)
[docs]defdoIt():"""Runs doIt on all current modifiers, similar to om2.MDGModifier.doIt(). """formodin_CURRENT_MODIFIER_LIST[:]:mod.doIt()
defensureModifierStackIsClear(_):"""Check if the modifier stack is empty, clear it with a warning if not. This is mainly used in some scene events like after new scene or before scene open, to make sure we have a clean start. """global_CURRENT_MODIFIER_LISTif_CURRENT_MODIFIER_LIST:warnings.warn(f"xmodifier list is not empty when it should! {_CURRENT_MODIFIER_LIST}",RuntimeWarning,)_CURRENT_MODIFIER_LIST=[]