# Copyright © 2023 Animal Logic. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.#
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
import contextlib
import warnings
import sys
import logging
from functools import wraps
from AL.omx.utils._stubs import cmds
from AL.omx.utils._stubs import om2
from AL.omx.utils._stubs import om2anim
from AL.omx import _xnode
from AL.omx.utils import _nodes
from AL.omx.utils import _modifiers
logger = logging.getLogger(__name__)
_CURRENT_MODIFIER_LIST = []
_JOURNAL_TOGGLE = None
[docs]
def setJournalToggle(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 = state
if state is None:
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]
def isJournalOn():
"""Query if we are actually recording journal for each creation or edit by omx.
"""
if _JOURNAL_TOGGLE is None:
return logger.isEnabledFor(logging.DEBUG)
return _JOURNAL_TOGGLE
[docs]
class JournalContext:
"""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.
"""
[docs]
def __init__(self, state=True):
self._state = state
self._oldState = _JOURNAL_TOGGLE
def __enter__(self):
setJournalToggle(self._state)
return self
def __exit__(self, *_, **__):
setJournalToggle(self._oldState)
class NodeCreationLog:
"""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 = False
def beginNewLog(self):
"""Adds a new list to the node creation log.
"""
self._log.append([])
self._isActive = True
def clearLogEntry(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.
"""
if clearAll:
self._log = []
else:
self._log.pop()
self._isActive = len(self._log) >= 1
def trackedNodes(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.
"""
if not self._log:
logger.warning("No tracked nodes in the creation log.")
return []
if queryAll:
return [node for trackedNodes in self._log for node in trackedNodes]
return self._log[-1]
def trackNode(self, node):
"""Add a node to the last active key in the tracking log.
Args:
node (:class:`XNode`): The node to track.
"""
if not self.isActive():
return
mobHandle = object.__getattribute__(node, "_mobHandle")
self._log[-1].append(mobHandle)
def isActive(self):
"""Check if the log is currently in use.
Returns:
bool: The active status of the log.
"""
return self._isActive
_NODE_CREATION_LOG = NodeCreationLog()
def startTrackingNodes():
"""Start a new entry in the node creation log to track any nodes created with createNode/createDagNode/createDGNode calls.
"""
_NODE_CREATION_LOG.beginNewLog()
def endTrackingNodes(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.
"""
if not _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)
return createdNodes
[docs]
def queryTrackedNodes(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.
"""
if not _NODE_CREATION_LOG.isActive():
logger.info("No active creation log to query.")
return []
return _NODE_CREATION_LOG.trackedNodes(queryAll)
[docs]
class TrackCreatedNodes:
"""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)
def wrapper(*args, **kw):
with self:
return func(*args, **kw)
return wrapper
def __enter__(self):
startTrackingNodes()
return self
def __exit__(self, *_, **__):
endTrackingNodes()
[docs]
def trackedNodes(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.
"""
return queryTrackedNodes(queryAll)
class XModifierLog:
__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 = method
self.values = values
def __str__(self):
return f"{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)
def wrapper(*args, **kwargs):
self = args[0]
# Add journal entry if needed, convert all MObjects to MObjectHandles
if isJournalOn():
values = inspect.getcallargs(method, *args, **kwargs)
del values["self"]
for k, v in values.items():
if isinstance(v, om2.MObject):
values[k] = om2.MObjectHandle(v)
self._journal.append(XModifierLog(method.__name__, values)) # NOQA
self._clean = False # NOQA
# Process args to convert any XNode to MObject
newArgs = []
for arg in args:
if isinstance(arg, _xnode.XNode):
arg = arg.object()
newArgs.append(arg)
for k, v in kwargs.items():
if isinstance(v, _xnode.XNode):
kwargs[k] = v.object()
logger.debug("Calling %s(%s, %s)", method, newArgs, kwargs)
res = method(*newArgs, **kwargs)
if self._immediate: # NOQA
self.doIt()
return res
return wrapper
[docs]
class XModifier:
""" 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 = immediate
self._reset()
def _reset(self):
self._modifier = _modifiers.MModifier()
self._journal = []
self._clean = True
[docs]
def journal(self):
"""Returns the current list of operations to run.
Returns:
list(str): A list of strings describing the operations to run.
"""
journal = []
for record in self._journal:
values = {}
for k, v in record.values.items():
if isinstance(v, om2.MObjectHandle):
mob = v.object()
if mob == om2.MObject.kNullObj:
values[k] = None
elif mob.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)}" for k, v in sorted(values.items())]
)
journal.append(f"mod.{record.method}({{{valuesStr}}})")
return journal
def _reallyDoIt(self, keepJournal=False):
logger.debug("%r._reallyDoIt() called", self)
try:
if self._immediate:
# this will call the self._modifier.doIt() method directly!
cmds.AL_OMXCommand()
else:
DoItModifierWrapper(self, self._modifier).doIt()
finally:
if logger.isEnabledFor(logging.DEBUG):
if isJournalOn():
logger.debug("Just called doIt on:\n%s", "\n".join(self.journal()))
else:
logger.debug("Just called doIt (No journal available)")
if not keepJournal:
self._journal = []
[docs]
def isClean(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.
"""
return self._clean
[docs]
def doIt(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)
if self._immediate and self not in _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)
if self._immediate:
# Create a new modifier to ensure modifiers are not shared across command instances
self._reset()
[docs]
def undoIt(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:
if self._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:
if logger.isEnabledFor(logging.DEBUG):
if isJournalOn():
logger.debug(
"Just called undoIt on:\n%s", "\n".join(self.journal())
)
else:
logger.debug("Just called undoIt (No journal available)")
if not keepJournal:
self._journal = []
[docs]
@_modifierMethod
def addAttribute(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
"""
if isinstance(node, _xnode.XNode):
node = node.object()
self._modifier.addAttribute(node, attribute)
return self
[docs]
@_modifierMethod
def addExtensionAttribute(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)
return self
[docs]
@_modifierMethod
def commandToExecute(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)
return self
[docs]
@_modifierMethod
def connect(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
"""
return self._modifier.connect(*args, **kwargs)
[docs]
@_modifierMethod
def createDGNode(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)
if nodeName:
# Call parent method directly to avoid an entry in the journal
self._modifier.renameNode(mob, nodeName)
# To get a valid MObjectHandle in XNode the creation needs to happen right away
self._modifier.doIt()
node = _xnode.XNode(mob)
_NODE_CREATION_LOG.trackNode(node)
return node
[docs]
@_modifierMethod
def createDagNode(
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.
"""
if parent is None:
parent = om2.MObject.kNullObj
if parent != om2.MObject.kNullObj:
if isinstance(parent, om2.MObject):
xparent = _xnode.XNode(parent)
elif isinstance(parent, _xnode.XNode):
xparent = parent
else:
xparent = _xnode.XNode(parent)
if not xparent.object().hasFn(om2.MFn.kTransform):
parent = xparent.bestFn().parent(0)
mob = self._modifier.createDagNode(typeName, parent=parent)
allCreated = [] if returnAllCreated else None
if parent == om2.MObject.kNullObj or parent is None:
self._modifier.doIt()
trn = mob
if manageTransformIfNeeded:
# Special case where Maya automatically creates and returns the parent transform instead of the newly created child.
trnFn = om2.MFnDagNode(trn)
if trnFn.childCount():
mob = trnFn.child(0)
availableName = _nodes.closestAvailableNodeName(typeName + "1")
self._modifier.renameNode(trn, availableName)
if returnAllCreated:
allCreated.append(_xnode.XNode(trn))
if nodeName:
# Call parent method directly to avoid an entry in the journal
self._modifier.renameNode(mob, nodeName)
# To get a valid MObjectHandle in XNode the creation needs to happen right away
self._modifier.doIt()
node = _xnode.XNode(mob)
_NODE_CREATION_LOG.trackNode(node)
if returnAllCreated:
allCreated.append(node)
return allCreated
return node
[docs]
@_modifierMethod
def createNode(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.
if kwargs.get("parent") or "dagNode" in cmds.nodeType(
typeName, inherited=True, isTypeName=True
):
return self.createDagNode(typeName, *args, **kwargs).object()
return self.createDGNode(typeName, *args, **kwargs).object()
[docs]
@_modifierMethod
def deleteNode(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)
return self
[docs]
@_modifierMethod
def disconnect(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)
return self
[docs]
@_modifierMethod
def linkExtensionAttributeToPlugin(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)
return self
[docs]
@_modifierMethod
def newPlugValue(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)
return self
[docs]
@_modifierMethod
def newPlugValueBool(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)
return self
[docs]
@_modifierMethod
def newPlugValueChar(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)
return self
[docs]
@_modifierMethod
def newPlugValueDouble(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)
return self
[docs]
@_modifierMethod
def newPlugValueFloat(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)
return self
[docs]
@_modifierMethod
def newPlugValueInt(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)
return self
[docs]
@_modifierMethod
def newPlugValueMAngle(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)
return self
[docs]
@_modifierMethod
def newPlugValueMDistance(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)
return self
[docs]
@_modifierMethod
def newPlugValueMTime(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)
return self
[docs]
@_modifierMethod
def newPlugValueShort(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)
return self
[docs]
@_modifierMethod
def newPlugValueString(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)
return self
[docs]
@_modifierMethod
def pythonCommandToExecute(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_)
return self
[docs]
@_modifierMethod
def removeAttribute(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)
return self
[docs]
@_modifierMethod
def removeExtensionAttribute(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)
return self
[docs]
@_modifierMethod
def removeExtensionAttributeIfUnset(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)
return self
[docs]
@_modifierMethod
def removeMultiInstance(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)
return self
[docs]
@_modifierMethod
def renameAttribute(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)
return self
[docs]
@_modifierMethod
def renameNode(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)
return self
[docs]
@_modifierMethod
def setNodeLockState(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)
return self
[docs]
@_modifierMethod
def unlinkExtensionAttributeFromPlugin(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)
return self
[docs]
@_modifierMethod
def reparentNode(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
"""
if not node.hasFn(om2.MFn.kDagNode):
raise TypeError(
"The XModifier.reparentNode() received non-DAG node to reparent."
)
nodeX = _xnode.XNode(node)
nodeFn = nodeX.basicFn()
if newParent is None or newParent == om2.MObject.kNullObj:
# If asked to reparent to world but it already is:
if not nodeFn.parentCount():
return self
newParent = None
else:
parentNodeX = _xnode.XNode(newParent)
# Avoid reparenting to itself:
if nodeX == parentNodeX:
raise RuntimeError(
"The XModifier.reparentNode() cannot reparent node to itself."
)
if not parentNodeX.hasFn(om2.MFn.kDagNode):
raise TypeError(
"The XModifier.reparentNode() received non-DAG node to reparent to."
)
# Avoid reparenting if it is already under the parent:
for i in range(nodeFn.parentCount()):
if _xnode.XNode(nodeFn.parent(i)) == parentNodeX:
return self
# Avoid reparenting parent to a child
if newParent:
for path in om2.MDagPath.getAllPathsTo(newParent):
while path.length() > 0:
if _xnode.XNode(path.pop().node()) == node:
raise RuntimeError(
"The XModifier.reparentNode() cannot reparent node to one of its children."
)
if not absolute:
newParent = om2.MObject() if not newParent else newParent
self._modifier.reparentNode(node, newParent)
return self
# 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 = ""
if newParent and om2.MObjectHandle(newParent).isValid():
newParentPath = om2.MFnDagNode(newParent).partialPathName()
flag = "-w" if not newParentPath else ""
cmdStr = f"parent -a {flag} {nodePath} {newParentPath}"
self._modifier.commandToExecute(cmdStr)
return self
class DoItModifierWrapper:
def __init__(self, xmod, mmod):
self._xmod = xmod
self._mmod = mmod
def doIt(self):
try:
self._mmod.doIt()
except Exception as e:
_, exc_value, _ = sys.exc_info()
if isJournalOn():
j = self._xmod.journal()
if not j:
logger.error("Failed to call doIt: %s", exc_value)
if len(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)
raise Exception(f"{exc_value} when calling {journal}") from e
raise Exception(
f"{exc_value} when calling doIt (journal unavailable)"
) from e
def undoIt(self):
try:
self._mmod.undoIt()
except Exception as e:
_, exc_value, _ = sys.exc_info()
if isJournalOn():
j = self._xmod.journal()
if not j:
logger.error("Failed to call undoIt: %s", exc_value)
elif len(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)
raise Exception(f"{exc_value} when calling {journal}") from e
raise Exception(
f"{exc_value} when calling undoIt (journal unavailable)"
) from e
def redoIt(self):
self.doIt()
def getAndClearModifierStack():
global _CURRENT_MODIFIER_LIST
existingMods = []
for xmod in _CURRENT_MODIFIER_LIST:
if isinstance(xmod, XModifier):
if xmod.isClean():
continue
mmod = xmod._modifier # NOQA
else:
mmod = None
logger.debug("Retrieving mod %r from list for execution", mmod)
existingMods.append(DoItModifierWrapper(xmod, mmod))
_CURRENT_MODIFIER_LIST = []
return existingMods
[docs]
def currentModifier():
"""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.
"""
if not _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]
return mod
[docs]
def newModifier():
"""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)
return mod
[docs]
def newAnimCurveModifier():
"""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)
return mod
def hasCurrentModifier():
"""Check if there is any omx modifier ready for an edit.
Returns:
bool: True if there is, False otherwise.
"""
return bool(_CURRENT_MODIFIER_LIST)
def executeModifiersWithUndo():
"""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.contextmanager
def newModifierContext():
"""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 context
for mod in _CURRENT_MODIFIER_LIST:
mod.doIt()
mod = XModifier(immediate=False)
logger.debug("Added modifier %r to list", mod)
_CURRENT_MODIFIER_LIST.append(mod)
yield mod
mod.doIt()
[docs]
@contextlib.contextmanager
def commandModifierContext(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_LIST and getattr(
_CURRENT_MODIFIER_LIST[-1], "_inOperation", False
)
if nested:
# 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()
yield mod
# 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-access
yield mod
mod._inOperation = False # pylint: disable=protected-access
try:
mod.doIt()
finally:
command._modifiers = getAndClearModifierStack() # NOQA
[docs]
def createDagNode(
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.
"""
return currentModifier().createDagNode(
typeName, parent=parent, nodeName=nodeName, returnAllCreated=returnAllCreated
)
[docs]
def createDGNode(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.
"""
return currentModifier().createDGNode(typeName, nodeName=nodeName)
[docs]
def doIt():
"""Runs doIt on all current modifiers, similar to om2.MDGModifier.doIt().
"""
for mod in _CURRENT_MODIFIER_LIST[:]:
mod.doIt()
def ensureModifierStackIsClear(_):
"""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_LIST
if _CURRENT_MODIFIER_LIST:
warnings.warn(
f"xmodifier list is not empty when it should! {_CURRENT_MODIFIER_LIST}",
RuntimeWarning,
)
_CURRENT_MODIFIER_LIST = []