# 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 logging
import functools
from AL.omx.utils._stubs import cmds
from AL.omx.utils._stubs import om2
from AL.omx.utils import _exceptions
logger = logging.getLogger(__name__)
[docs]
def createAttributeDummy():
"""Create a dummy node with a message attribute called 'kMessage', this is
often for a connection to force Maya to create an element plug etc.
Notes:
Usually this temp node will need to be removed soon after the attribute is used.
More info in the notes for :func:`getOrExtendMPlugArray()`.
Returns:
om2.MObject: The MObject created.
"""
dagMod = om2.MDagModifier()
dummyNode = dagMod.createNode("transform")
dagMod.doIt()
fnMsgAttr = om2.MFnMessageAttribute()
attr = fnMsgAttr.create("kMessage", "kMessage")
depFn = om2.MFnDependencyNode(dummyNode)
depFn.addAttribute(attr)
return dummyNode
[docs]
def getOrExtendMPlugArray(arrayPlug, logicalIndex, dummy=None):
"""Get the element plug by logicIndex if the element exists, otherwise
extend the array until the logical index exists.
Args:
arrayPlug (om2.MPlug): A valid array plug.
logicalIndex (int): The logical index.
dummy (None | om2.MObject, optional): The dummy MObject that contains a 'kMessage',
createAttributeDummy() will be called to create one if it is None.
Notes:
Using this function without a persistent dummy in quick iterations
will most likely hard crash Maya for you as it struggles to create,
connect from and delete nodes repeatedly in a short span of time.
Use autoDummy (dummy argument = None) sparingly and only if you are certain
this runs with plenty elbow room around it or only once.
Returns:
om2.MPlug: The plug at the logical index, or None if it is not a valid array plug.
"""
cleanUpDummy = False
if dummy is None:
dummy = createAttributeDummy()
cleanUpDummy = True
# We catch a string that findPlug can't resolve here.
# Chances are the plug is a subplug of an array, a compound,
# or a nesting of a combination of both
if not arrayPlug.isArray or logicalIndex < 0:
return None
aCount = arrayPlug.evaluateNumElements()
if aCount >= (logicalIndex + 1):
return arrayPlug.elementByLogicalIndex(logicalIndex)
dummyDestination = findPlug("kMessage", dummy)
if not dummyDestination:
logger.error("Dummy has no kMessage attribute.")
return None
dg_mod = om2.MDGModifier()
for i in range(aCount, logicalIndex + 1):
srcPlug = arrayPlug.elementByLogicalIndex(i)
dg_mod.connect(srcPlug, dummyDestination)
dg_mod.disconnect(srcPlug, dummyDestination)
dg_mod.doIt()
if cleanUpDummy:
dg_mod.deleteNode(dummy)
dg_mod.doIt()
return arrayPlug.elementByLogicalIndex(logicalIndex)
def _findLeafPlug(plug, iterCount=None, maxRecursion=8, relaxed=False):
""" Given a plug it will walk the plug down compound or array elements
until a leaf plug (one not of type compound or array) is found.
Args:
plug (om2.MPlug): the root plug to recursively walk
iterCount (int, optional): this is just to ensure an iteration lock to prevent odd
infinite loops, something for the function, when it runs
recursively, to pass back to itself and increment
maxRecursion (int, optional): the maximum number of recursions allowed
relaxed (bool, optional): Whether we keep silent on the potential error
Raises:
StopIteration: If iteration exceeds the maxRecursion.
:class:`PlugArrayOutOfBounds`: If the plug is an array plug with no elements.
Returns:
None : If there is an error (shouldn't return).
om2.MPlug: Of the leaf if one is found.
om2.MPlug (of the argument plug): If the supplied plug was already a leaf.
"""
if iterCount is None:
iterCount = 0
if iterCount >= maxRecursion:
raise StopIteration(
f"max recursion limit reached at {iterCount} without finding a viable leaf plug"
)
iterCount += 1
# It's important the array test runs first
if plug.isArray:
plugCount = plug.evaluateNumElements()
if plugCount <= 0:
if not relaxed:
raise _exceptions.PlugArrayOutOfBounds(
f"finding the leaf plug of {plug.name()} failed as it's an unitialized array attribute"
)
plugCount = 1
for i in range(plugCount):
subPlug = plug.elementByLogicalIndex(i)
return _findLeafPlug(subPlug, iterCount, maxRecursion, relaxed)
if plug.isCompound:
for i in range(plug.numChildren()):
subPlug = plug.child(i)
return _findLeafPlug(subPlug, iterCount, maxRecursion, relaxed)
return plug
[docs]
def findSubplugByName(plug, token):
""" Give a plug recursively through all nested compounds looking for plug by name
Args:
plug (om2.MPlug): The compound plug.
token (str): The attribute name / path
Returns:
om2.MPlug: The child / descendent plug.
"""
if not plug.isCompound:
return None
if not token:
return plug
attrPath = plug.partialName(includeNodeName=False, useFullAttributePath=True)
plugName = f"{attrPath}.{token}"
return findPlug(plugName, plug.node())
def _findPlugOnNodeInternal(mob, plugName, networked=False, relaxed=False):
"""
Args:
mob (om2.MObject): Maya MObject for a node we want to find the plug on
plugName (str): string for the name of the plug we want to look for. Paths supported
networked (bool, optional): pass-through or emulation for Maya's argument that will
ensure the only plugs returned are networked ones
relaxed (bool, optional): clients of this function potentially deal with large sets of data
that might contain None, False, or something else flagging
an uninteresting index. Relaxed will ensure invalid mobs will
be silently skipped if set to True
Returns:
om2.MPlug: the plug found unless it outright fails.
Caller needs to ensure it's not null to know if it's valid or not
Notes:
The "networked" argument would be better named ifNetworked or ifConnected,
but we chose to remain consistent with Maya's semantics and choices over our own.
"""
# First we validate mob
if mob is None or (isinstance(mob, om2.MObject) and mob.isNull()):
if not relaxed:
raise ValueError("Parameter mob is MObject.kNullObj or None")
logger.error("Parameter mob is null or None")
return om2.MPlug()
try:
dep = om2.MFnDependencyNode(mob)
except Exception as e:
# depNode constructor failure, object is not a MObject
if not relaxed:
raise ValueError("Parameter mob is not a valid MObject") from e
logger.error("Parameter mob is not a valid MObject")
return om2.MPlug()
# Now we find the plug
try:
return dep.findPlug(plugName, networked)
except RuntimeError as e:
# We catch a string that findPlug can't resolve here.
# Chances are the plug is a subplug of an array, a compound,
# or a nesting of a combination of both
cleanUpDummy = False
dummy = None
compoundSplit = plugName.split(".") # any compounding will be dot delimited
currPlug = None
firstRun = False
for token in compoundSplit: # for each subPlug, even if just one, in the path
aIndex = None
# mutate token and store index if it's an array plug
if token[-1] == "]":
trimPoint = token.rfind("[") + 1
aIndex = int(token[trimPoint:-1])
token = token[: trimPoint - 1]
if currPlug is None: # first traversal
try:
currPlug = dep.findPlug(
token, False
) # this covers compound or head of array
except RuntimeError as err:
if not relaxed:
logger.error("Cannot find plug: %s", token)
logger.error(err)
return om2.MPlug() # early escape
firstRun = True
if aIndex is not None: # if it's an array plug...
if currPlug is None or currPlug.isNull:
if relaxed:
logger.warning(
"Error finding currPlug on plug: %s, root token %s not found on node %s",
plugName,
token,
dep.name(),
)
return om2.MPlug()
raise RuntimeError(
f"findplug failed in finding array entry for plugname {plugName}"
) from e
if (
aIndex != 0
and aIndex not in currPlug.getExistingArrayAttributeIndices()
):
if not relaxed:
raise _exceptions.PlugArrayOutOfBounds(
f"plug {plugName} was queried for unavailable index {aIndex} on node {dep.name()}"
) from e
dummy = createAttributeDummy()
cleanUpDummy = True
if dummy is not None:
currPlug = getOrExtendMPlugArray(currPlug, aIndex, dummy)
else:
currPlug = currPlug.elementByLogicalIndex(aIndex)
if not firstRun and currPlug.isCompound:
# We care for compounding only when we get to some level where it's not
# an Array any longer, otherwise arrays of compounds would always overextend and fail
currPlug = findSubplugByName(currPlug, token)
else:
firstRun = False
if cleanUpDummy:
dg_mod = om2.MDGModifier()
dg_mod.deleteNode(dummy)
dg_mod.doIt()
# This section emulates Maya's "isNetworked" flag in findPlug()
if not networked:
return currPlug
# This should only ever be reached if a plug was found, and isNetworked is on
# which means we want to ensure that the plug is connected before we return it
if currPlug and currPlug.isConnected:
return currPlug
# Plug exists, but isNetworked is on and the plug is disconnected
return om2.MPlug()
[docs]
def findPlug(plugName, node=None):
""" Find the om2.MPlug by name (and node)
It allows to pass either attribute name plus node om2.MObject
or the full plug path without a node.
Args:
plugName (str): plug name or node.attr
node(om2.MObject, optional): node of the plug, it is optional.
Returns:
om2.MPlug if found None otherwise
"""
# most of time, MFnDependencyNode.findPlug() will work and be faster, but just
# be aware of the behavior differences between the two:
# 1. We need to check and return None for a plug whose node is invalid (e.g. deleted),
# as MFnDependencyNode.findPlug() will still return a valid plug even when the
# node has been removed, MSelectionList.getPlug() does the check for you.
# 2. MSelectionList supports nodeName.plugName notion, even with [] or . for array element
# plug or child plug full path, MFnDependencyNode.findPlug() does not.
# 3. MSelectionList always returns non-networked plug, while MFnDependencyNode.findPlug()
# depends on the argument you passed in.
# 4. MSelectionList.getPlug() will find a plug on a child shape node if such plug does not
# exist on a transform, when you passed in "transformNodeName.shapeAttrName", which
# doesn't sound right but client code might expect that behavior.
if node and not node.isNull() and om2.MObjectHandle(node).isValid():
fnDep = om2.MFnDependencyNode(node)
# Avoid processing child plug full path, array element, or plug on child shape.
if fnDep.hasAttribute(plugName):
try:
# We need to use non-network plug as you can never know if the plug
# will be disconnected later.
plug = fnDep.findPlug(plugName, False)
return None if plug.isNull else plug
except Exception:
return _findPlugOnNodeInternal(node, plugName, relaxed=True)
# Then we deal with complex plug path, e.g. compound.child, array[index], etc.
if node:
if node.hasFn(om2.MFn.kDagNode):
nodeName = om2.MFnDagNode(node).fullPathName()
else:
nodeName = om2.MFnDependencyNode(node).absoluteName()
plugName = f"{nodeName}.{plugName}"
# to avoid expensive check for existence, wrap with try except...
# A cmds.objExists() test will return True for duplicate-named nodes, but it will
# still raise run-time error when you add them to om2.MSelectionList.
sl = om2.MSelectionList()
try:
sl.add(plugName)
except RuntimeError as e:
logger.debug("Plug: %r does not exist. \n%s", plugName, e)
return None
if sl.length() == 1:
try:
plug = sl.getPlug(0)
except TypeError:
# The following is a workaround for a bug in MSelectionList.getPlug()
# We reported it in Nov 2019 and can remove this as soon as a patch is released.
# To reproduce it, do a 'getPlug()' on 'someJoint.rotatePivot' for example.
# or on 'someCurveShape.cp[0]'
if not node:
node = sl.getDependNode(0)
fnDep = om2.MFnDependencyNode(node)
plugNameOnly = plugName[plugName.find(".") + 1 :]
try:
plug = fnDep.findPlug(plugNameOnly, True)
except Exception:
if cmds.objExists(plugName):
# we should never end up here...
logger.debug(
"""
We should not end up here.
This is a bug a plug that refuses to be found using
MSelectionList.getPlug() or MFnDependencyNode.findPlug()
PlugName: %s
""",
plugName,
)
plug = _findPlugOnNodeInternal(node, plugName, relaxed=True)
else:
return None
if not plug.isNull:
return plug
logger.error('Plug: "%s" is Null.', plugName)
elif sl.length() > 1:
logger.error('Plug: "%s" is not unique.', plugName)
return None
[docs]
def plugIsValid(plug):
"""Checks for plug validity working around various Maya issues. See notes.
Notes:
Courtesy of Maya having issues when a plug;
- can be obtained that is fully formed
- responds to methods such as isCompound, isElement etc.
- also respond negatively to isNull BUT actually has an uninitialized array in its stream and will
therefore hard crash if queried for anything relating to its array properties such as numElements etc.
Args:
plug (om2.MPlug): The plug to do validity check.
Returns:
bool: True if it is a valid plug, False otherwise.
"""
if (not plug) or plug.isNull:
return False
if plug.isChild:
return plugIsValid(plug.parent())
if not plug.isElement:
return True
if plug.logicalIndex() == -1: # kInvalidIndex:
return False
return plugIsValid(plug.array())
[docs]
def plugEnumNames(plug):
"""Get enum name list from an enum plug, None otherwise.
Args:
plug (om2.MPlug): the enum plug to get the enum name list.
Returns:
tuple | None: A tuple of enum names, None if failed.
"""
if not plugIsValid(plug):
return None
attrType, fn = attributeTypeAndFnFromPlug(plug)
if attrType == om2.MFn.kEnumAttribute:
return tuple([fn.fieldName(i) for i in range(fn.getMin(), fn.getMax() + 1)])
return None
[docs]
def nextAvailableElementIndex(plug):
"""Get the next available element index that does not exist yet.
Args:
plug (om2.MPlug): the array plug to get the index for.
Returns:
int: the next available element index, -1 if failed.
"""
if not plugIsValid(plug) or not plug.isArray:
return -1
indices = plug.getExistingArrayAttributeIndices()
idx = max(indices) + 1 if indices else 0
idx = [i for i in range(idx + 1) if i not in indices][0]
return idx
[docs]
def nextAvailableElement(plug):
"""Get the next available element plug that does not exist yet.
Args:
plug (om2.MPlug): the array plug to get the element plug for.
Returns:
om2.MPlug: the next available element plug, or null plug if failed.
"""
idx = nextAvailableElementIndex(plug)
return plug.elementByLogicalIndex(idx)
def _plugLockElision(f):
""" Internal use only. It is a decorator to enable functions operating on plugs as
first argument or plug keyed argument to elide eventual locks present on the plug
Args:
f (function): the python function to wrap.
Notes:
This requires the functor getting decorated has a argument call "plug" and "_wasLocked".
Examples:
@_plugLockElision
def someFunction(plug, ...)
"""
@functools.wraps(f)
def wrapper(*args, **kwargs):
plug = kwargs.get("plug", args[0])
isValidPlug = plugIsValid(plug)
if isValidPlug:
restoreLocked = plug.isLocked
plug.isLocked = False
else:
restoreLocked = False
logger.warning(
"Invalid plug: %s. Skipping plug unlocking for %s", plug, f.__name__
)
result = f(*args, _wasLocked=restoreLocked, **kwargs)
if isValidPlug:
plug.isLocked = restoreLocked
return result
return wrapper
_ATTRIBUTE_MFUNCTORS_BY_TYPE = {
om2.MFn.kNumericAttribute: om2.MFnNumericAttribute,
om2.MFn.kUnitAttribute: om2.MFnUnitAttribute,
om2.MFn.kEnumAttribute: om2.MFnEnumAttribute,
om2.MFn.kTypedAttribute: om2.MFnTypedAttribute,
om2.MFn.kMatrixAttribute: om2.MFnMatrixAttribute,
om2.MFn.kMessageAttribute: om2.MFnMessageAttribute,
om2.MFn.kCompoundAttribute: om2.MFnCompoundAttribute,
om2.MFn.kGenericAttribute: om2.MFnGenericAttribute,
om2.MFn.kLightDataAttribute: om2.MFnLightDataAttribute,
}
[docs]
def iterAttributeFnTypesAndClasses():
"""Iter through all the attribute MFn types and its MFn*Attribute classes.
"""
for fnType, fnCls in _ATTRIBUTE_MFUNCTORS_BY_TYPE.items():
yield fnType, fnCls
[docs]
def attributeTypeAndFnFromPlug(plug):
"""Get the attribute type and MFn*Attribute class for the plug.
Args:
plug (om2.MPlug): The plug to query type and attribute functor for.
Returns:
om2.MFn.*, om2.MFn*Attribute.
"""
attr = plug.attribute()
retFn = None
retType = om2.MFn.kAttribute
for attrType, fn in iterAttributeFnTypesAndClasses():
if attr.hasFn(attrType):
retFn = fn(attr)
retType = attrType
break
if retFn is None:
retFn = om2.MFnAttribute(attr)
return retType, retFn
[docs]
def valueFromPlug(
plug,
context=om2.MDGContext.kNormal,
faultTolerant=True,
flattenComplexData=True,
returnNoneOnMsgPlug=False,
asDegrees=False,
):
"""Get the value of the plug.
Args:
plug (om2.MPlug): The plug to get the value from.
context (om2.MDGContext, optional): The DG context to get the value for.
faultTolerant (bool, optional): Whether to raise on errors or proceed silently
flattenComplexData (bool, optional): Whether to convert MMatrix to list of doubles
returnNoneOnMsgPlug (bool, optional): Whether we return None on message plug or the plug itself.
asDegrees (bool, optional): For an angle unit attribute we return the value in degrees
or in radians.
Returns:
om2.MFn.*, om2.MFn*Attribute.
"""
if plug.isArray:
numElem = plug.evaluateNumElements()
indices = plug.getExistingArrayAttributeIndices()
arrLength = 1 + max(indices) if indices else numElem
arr = [None] * arrLength
for i in plug.getExistingArrayAttributeIndices():
arr[i] = valueFromPlug(
plug.elementByLogicalIndex(i),
context=context,
faultTolerant=faultTolerant,
flattenComplexData=flattenComplexData,
returnNoneOnMsgPlug=returnNoneOnMsgPlug,
asDegrees=asDegrees,
)
return arr
if plug.isCompound:
serialiseAsDict = True
if plug.attribute().hasFn(om2.MFn.kNumericAttribute):
attrFn = om2.MFnNumericAttribute(plug.attribute())
if attrFn.numericType() != om2.MFnNumericData.kInvalid:
serialiseAsDict = False
childPlugs = (plug.child(i) for i in range(plug.numChildren()))
if serialiseAsDict:
return {
om2.MFnAttribute(childPlug.attribute()).name: valueFromPlug(
childPlug,
context=context,
faultTolerant=faultTolerant,
flattenComplexData=flattenComplexData,
returnNoneOnMsgPlug=returnNoneOnMsgPlug,
asDegrees=asDegrees,
)
for childPlug in childPlugs
}
return [
valueFromPlug(
childPlug,
context=context,
faultTolerant=faultTolerant,
flattenComplexData=flattenComplexData,
returnNoneOnMsgPlug=returnNoneOnMsgPlug,
asDegrees=asDegrees,
)
for childPlug in childPlugs
]
if plug.attribute().hasFn(om2.MFn.kMessageAttribute) and returnNoneOnMsgPlug:
return None
valAndTypes = valueAndTypesFromPlug(
plug,
context=context,
faultTolerant=faultTolerant,
flattenComplexData=flattenComplexData,
asDegrees=asDegrees,
)
if isinstance(valAndTypes, tuple) and len(valAndTypes) == 3:
val, _, _ = valAndTypes
return val
return None
def __num_as_float(t):
"""Internal use only, solely meant for code compression
"""
return t in (om2.MFnNumericData.kFloat, om2.MFnNumericData.kDouble)
def __num_as_int(t):
"""Internal use only, solely meant for code compression
"""
# in order of likelyhood/speed centric
return (
t == om2.MFnNumericData.kInt
or (om2.MFnNumericData.kByte <= t <= om2.MFnNumericData.kShort)
or t == om2.MFnNumericData.kLong
or t == om2.MFnNumericData.kInt64
or t == om2.MFnNumericData.kAddr
)
[docs]
def nodeDotAttrFromPlug(plug):
"""Return nodeName.attribute str representation of the plug.
Notes:
plug.partialName() will not give you a unique nodeName.attribute if the
node is duplicatly named.
Args:
plug (om2.MPlug): A Maya MPlug
Returns:
str: the nodePartialPathName.attribute
"""
if not plug or plug.isNull:
return None
node = plug.node()
if node.hasFn(om2.MFn.kDagNode):
nodename = om2.MFnDagNode(node).partialPathName()
else:
nodename = om2.MFnDependencyNode(node).name()
attrName = plug.partialName(includeNodeName=False, useFullAttributePath=True)
return f"{nodename}.{attrName}"
[docs]
def valueAndTypesFromPlug(
plug,
context=om2.MDGContext.kNormal,
exclusionPredicate=lambda x: False,
inclusionPredicate=lambda x: True,
faultTolerant=True,
flattenComplexData=True,
asDegrees=False,
):
"""Retrieves the value, attribute functor type, and attribute type per functor for any given plug.
Args:
plug (om2.MPlug): A maya type plug of any description
context (om2.MDGContext, optional): The maya context to retrieve the plug at. This isn't always applicable,
and w indicate so in the switches when it's not, but it's always accepted
When a context isn't passed the default is kNormal, which is current
context at current time.
exclusionPredicate (function, optional): Predicated on the attribute functor internal to the function
this enables us to filter by an internal which preceeds the plug check stages, enabling things
such as early filtering of hidden attributes or factory ones etc.
inclusionPredicate (function, optional): Predicated on the attribute functor internal to the function
this enables us to filter by an internal which preceeds the plug check stages, enabling things
such as early filtering by specific attribute functor calls before choosing to opt in.
faultTolerant (bool, optional): Whether to raise on errors or proceed silently.
flattenComplexData (bool, optional): Whether to convert MMatrix to list of doubles.
asDegrees (bool, optional): For an angle unit attribute we return the value in degrees
or in radians.
Returns:
tuple | None: This will return None if it doesn't raise but is unable to read.
E.G. a predicate is off value or the plug is a compound.
If something of note is found it's returned
as a triplet of value, fnType, attrType, with value being potentially an MPlug
Todo:
Escalate faultTolerant to have levels for maya failures vs finer grained failures
"""
t = None # subtype that should be shadowed in most sub-scopes
if not plugIsValid(plug):
if faultTolerant:
return None, 0, 0
raise _exceptions.PlugMayaInvalidException(
plug,
"valueAndTypesFromPlug() failure in non fault tolerant mode, plug is most likely not value initialised",
)
attrType, fn = attributeTypeAndFnFromPlug(plug)
if attrType == om2.MFn.kCompoundAttribute:
if faultTolerant:
return None, 0, 0
raise _exceptions.PlugMayaInvalidException(
plug,
"valueAndTypesFromPlug() failure, nonFactory or nonUniformly typed compound type attribute",
)
logger.debug("Found attribute type %s", attrType)
try:
if exclusionPredicate(fn) or not inclusionPredicate(fn):
return None, 0, 0
except Exception as e:
# Yes, except all, but we want to catch every possible predicate error and indicate the predicate failed
# since it's all one should care about as they can very easily debug those predicates in isolation,
# and should in fact have ensured they didn't ever fail before using them
raise _exceptions.PlugAttributePredicateError(plug) from e
if plug.isArray:
count = plug.evaluateNumElements()
retVal = [None] * count
# @note: Wwhile it might seem logical to return the types once, and only the data
# as an array, which is what the old beast utils one did, it's worth
# noting that there exists a rare case with typed attributes where
# you can have an array of mixed types, therefore to retain
# a universal signature and to make each granular
# return identical regardless of whether it comes from an array or not
# we choose to return an array of triples instead of a triplet with one
# array of values in it
for i in range(count):
retVal[i] = valueAndTypesFromPlug(
plug.elementByLogicalIndex(i),
context,
faultTolerant=faultTolerant,
flattenComplexData=flattenComplexData,
asDegrees=asDegrees,
)
return retVal
# ----------- NUMERIC -----------
contextWarnMsg = "%s requested with non normal context, for Maya this info can't be provided, providing normal context instead for plug %s"
if attrType == om2.MFn.kNumericAttribute:
t = fn.numericType()
if t == om2.MFnNumericData.kBoolean:
return plug.asBool(), attrType, t
if __num_as_float(t):
return plug.asFloat(), attrType, t
if __num_as_int(t):
# @note this exception is necessary because
# maya has plenty factory plugs that are in and failing on geometry
# usually edges or faces
try:
return plug.asInt(), attrType, t
except RuntimeError as e:
if fn.hidden:
return None, attrType, t
raise RuntimeError(
"maya error trying to retrieve flawed int plug that's not hidden"
) from e
# From this point on context is spottily supported, or not supported at all in Maya
# as well as crashy, so we don't forward it any longer
if context != om2.MDGContext.kNormal:
# @TODO: raise custom error if not fault tolerant
logger.warning(contextWarnMsg, "aggregate numeric attribute", plug.name())
dataHandle = plug.asMDataHandle()
val = None
if t == om2.MFnNumericData.k2Short:
val = dataHandle.asShort2()
elif t == om2.MFnNumericData.k3Short:
val = dataHandle.asShort3()
elif t == om2.MFnNumericData.k2Int: # same as long
val = dataHandle.asInt2()
elif t == om2.MFnNumericData.k3Int: # same as long
val = dataHandle.asInt3()
elif t == om2.MFnNumericData.k2Float:
val = dataHandle.asFloat2()
elif t == om2.MFnNumericData.k3Float:
val = dataHandle.asFloat3()
elif t == om2.MFnNumericData.k2Double:
val = dataHandle.asDouble2()
elif t == om2.MFnNumericData.k3Double:
val = dataHandle.asDouble3()
elif t == om2.MFnNumericData.k4Double:
val = [None, None, None, None]
childCount = plug.numChildren()
# Nullify exists to maintain consistency with all other cases where val should be None when
# unsupported or unrecognised. Because we need to form val before the loop we also need
# to ensure that it's restored to plain None were the range to be 0 for some reason,
# or we would be returning a deceiving structure and potentially lead to nasty and
# silent later failure in caller's code
nullify = True
for i in range(childCount):
nullify = False
currChildPlug = plug.child(i)
val[i] = currChildPlug.asFloat()
if nullify:
val = None
if val is None and not faultTolerant:
raise _exceptions.PlugUnhandledTypeException(
plug, attrType, t, "this seems to be an unhandled numeric attribute"
)
return val, attrType, t
# ------------ UNIT -------------
if attrType == om2.MFn.kUnitAttribute:
t = fn.unitType()
if t == om2.MFnUnitAttribute.kAngle:
if not asDegrees:
return plug.asMAngle().asRadians(), attrType, t
return plug.asMAngle().asDegrees(), attrType, t
if t == om2.MFnUnitAttribute.kDistance:
return plug.asDouble(), attrType, t
if t == om2.MFnUnitAttribute.kTime:
return plug.asMTime().value, attrType, t
# ----------- MATRIX ------------
if attrType == om2.MFn.kMatrixAttribute:
attrData = om2.MFnMatrixData(plug.asMObject()).matrix()
if flattenComplexData:
return [attrData[i] for i in range(len(attrData))], attrType, fn.kDouble
return attrData, attrType, fn.kDouble
if attrType == om2.MFn.kFloatMatrixAttribute:
attrData = om2.MFnMatrixData(plug.asMObject()).matrix()
if flattenComplexData:
return [attrData[i] for i in range(len(attrData))], attrType, fn.kFloat
return attrData, attrType, fn.kFloat
# ------------ ENUM -------------
if attrType == om2.MFn.kEnumAttribute:
return plug.asShort(), attrType, 1
# ----------- TYPED -------------
if attrType == om2.MFn.kTypedAttribute:
t = fn.attrType()
logger.debug("Found typed attribute %s", t)
if t == om2.MFnData.kString: # string
return plug.asString(), attrType, t
if t == om2.MFnData.kStringArray: # string array
if context != om2.MDGContext.kNormal:
# @TODO: raise custom error if not fault tolerant
logger.warning(
contextWarnMsg, "typed attribute of type stringArray", plug.name()
)
dataHandle = plug.asMDataHandle()
dataMob = dataHandle.data()
if dataMob.isNull():
# @TODO: should we maybe return None or something like that? Tricky
# empty list does make it more functional
return [], attrType, t
fnData = om2.MFnStringArrayData(dataHandle.data())
return fnData.array(), attrType, t
if t == om2.MFnData.kFloatArray: # float array
if context != om2.MDGContext.kNormal:
# @TODO: raise custom error if not fault tolerant
logger.warning(
contextWarnMsg, "typed attribute of type floatArray", plug.name()
)
# om2 has no way to get a floatArray value as MFnFloatArrayData is still missing in om2 ...
# We have to use cmds here:
attrPath = nodeDotAttrFromPlug(plug)
return cmds.getAttr(attrPath), attrType, t
if t == om2.MFnData.kIntArray: # int array
if context != om2.MDGContext.kNormal:
logger.warning(
contextWarnMsg, "typed attribute of type intArray", plug.name()
)
dataHandle = plug.asMDataHandle()
dataMob = dataHandle.data()
if dataMob.isNull():
# @TODO: should we maybe return None or something like that? Tricky
# empty list does make it more functional
return [], attrType, t
fnData = om2.MFnIntArrayData(dataHandle.data())
arrayData = fnData.array()
if flattenComplexData:
return list(arrayData), attrType, t
return arrayData, attrType, t
if t == om2.MFnData.kDoubleArray: # double array
if context != om2.MDGContext.kNormal:
# @TODO: raise custom error if not fault tolerant
logger.warning(
contextWarnMsg, "typed attribute of type doubleArray", plug.name()
)
dataHandle = plug.asMDataHandle()
dataMob = dataHandle.data()
if dataMob.isNull():
# @TODO: should we maybe return None or something like that? Tricky
# empty list does make it more functional
return [], attrType, t
fnData = om2.MFnDoubleArrayData(dataHandle.data())
arrayData = fnData.array()
if flattenComplexData:
return list(arrayData), attrType, t
return arrayData, attrType, t
if t == om2.MFnData.kMatrix: # matrix as list
mtx = om2.MFnMatrixData(plug.asMObject()).matrix()
if flattenComplexData:
return list(mtx), attrType, t
return mtx, attrType, t
if t == om2.MFnData.kVectorArray: # vector array
# This is an absolutely terrifying attribute.
# For performance reasons it will handle things around by reference.
if context != om2.MDGContext.kNormal:
# @TODO: raise custom error if not fault tolerant
logger.warning(
contextWarnMsg, "typed attribute of type vectorArray", plug.name()
)
dataHandle = plug.asMDataHandle()
dataMob = dataHandle.data()
if dataMob.isNull():
# @TODO: should we maybe return None or something like that? Tricky
# empty list does make it more functional
return [], attrType, t
fnData = om2.MFnVectorArrayData(dataHandle.data())
# There is a really tough decision being made here.
# Naturally the functor's data method returns a live reference to the MVectorArray
# that describe the data, which is supremely dangerous, but sensible given
# the cases such attributes are normally used in.
# A separate function will be provided to get this attribute as a live point array,
# and in here we choose to instead return a plain list of lists
liveMayaArray = fnData.array()
return [[p.x, p.y, p.z] for p in liveMayaArray], attrType, t
if t == om2.MFnData.kPointArray: # point array
# For performance reasons it will handle things around by reference.
if context != om2.MDGContext.kNormal:
# @TODO: raise custom error if not fault tolerant
logger.warning(
contextWarnMsg, "typed attribute of type pointArray", plug.name()
)
dataHandle = plug.asMDataHandle()
dataMob = dataHandle.data()
if dataMob.isNull():
# @TODO: should we maybe return None or something like that? Tricky
# empty list does make it more functional
return [], attrType, t
fnData = om2.MFnPointArrayData(dataHandle.data())
# There is a really tough decision being made here.
# Naturally the functor's data method returns a live reference to the MPointArray
# that describe the data, which is supremely dangerous, but sensible given
# the cases such attributes are normally used in.
# A separate function will be provided to get this attribute as a live point array,
# and in here we choose to instead return a plain list of lists
liveMayaArray = fnData.array()
return [[p.x, p.y, p.z, p.w] for p in liveMayaArray], attrType, t
# ------ invalid ------
if t == om2.MFnData.kInvalid:
if faultTolerant:
return None, attrType, t
# @todo: replace with custom exception as well as re-implement
# once fault tolerance becomes a scaling argument instead
# of a boolean.
raise RuntimeError("attempted to fetch invalid data type")
# ------ everything else ------
# unhandled types such as lattice, geo sources etc.
return om2.MPlug().copy(plug), attrType, 1
# ----------- MESSAGE -----------
if attrType == om2.MFn.kMessageAttribute:
# @todo: Do we want to return the plug itself when it's actually a message?
# The only real use for a message is its connections usually,
# which means this would make good sense, but this might affect use as serialisation
#
return om2.MPlug().copy(plug), attrType, 1
# ----------- GENERIC -----------
if attrType == om2.MFn.kGenericAttribute:
# For the unitConversion.input/unitConversion.output plug, it is generic attribute.
# So here we have to use cmds...
value = cmds.getAttr(plug.partialName(includeNodeName=True))
return value, attrType, -1
raise _exceptions.PlugUnhandledTypeException(plug, attrType, t)
[docs]
@_plugLockElision
def setValueOnPlug(
plug,
value,
elideLock=False,
exclusionPredicate=lambda x: False,
inclusionPredicate=lambda x: True,
faultTolerant=True,
_wasLocked=False,
modifier=None,
doIt=True,
asDegrees=False,
):
"""Set plug value using a modifier.
Args:
plug (om2.MPlug): The plug to set to the specified value contained in the following argument.
value (any): the value to set the plug to
elideLock (bool, optional): Whether we unlock the plug (only) during value setting.
exclusionPredicate (function, optional): See valueAndTypesFromPlug
inclusionPredicate (function, optional): See valueAndTypesFromPlug
faultTolerant (bool, optional): Whether to raise on errors or proceed silently
_wasLocked (bool, optional): internal use only, this is required for plugLock eliding
functions to play nice with elision conditions, since the decorator
will always run first and unlock the plug, and therefore has to be
able to signal the wrapped function of the previous state of the plug
for both the check AND restoration of the lock before an eventual exception
modifier (om2.MDGModifier, optional): to support modifier for undo/redo purpose.
doIt (bool, optional): True means modifier.doIt() will be called immediately to apply the plug value change.
False enable you to defer and call modifier.doIt() later in one go.
asDegrees (bool, optional): When it is an angle unit attribute, if this is True than we take the
value as degrees, otherwise as radians. This flag has no effect
when it is not an angle unit attribute.
Returns:
None if for whatever reason the operation is invalid or has effected no change
Otherwise it will return the original value (so it can be stored for undo support).
Normally the pattern would be None as invalid op, False for no change,
and True for change affected, but given the previous value for a boolean
attribute could reasonably be False as a value we have to use None for both
invalid as well as ineffective, and rely on the user to invoke the call
as non fault tolerant and catching specific exceptions for no-change
Todo:
In support of the last note in the return description we need fine grained exceptions
to enable this function to act in a ternary fashion
"""
logger.debug("setValueOnPlug(plug=%s, value=%s, doIt=%s)", plug, value, doIt)
if not plugIsValid(plug):
if faultTolerant:
return None
raise _exceptions.PlugMayaInvalidException(
plug,
"invalid plug found before trying to set value on it from setValueOnPlug",
)
if not elideLock and _wasLocked:
if faultTolerant:
return None
raise _exceptions.PlugLockedForEditError(plug)
attrType, fn = attributeTypeAndFnFromPlug(plug)
if exclusionPredicate(fn) or not inclusionPredicate(fn):
return None
if not modifier:
modifier = om2.MDGModifier()
doIt = True
if (plug.isArray or plug.isCompound) and isinstance(
value, (list, tuple, om2.MVector, om2.MFloatArray, om2.MDoubleArray)
):
if plug.isArray:
count = len(value)
else:
count = plug.numChildren()
if count < len(value):
if faultTolerant:
return None
raise RuntimeError(
"value is a sequence of length different than the length of the array / compound children length of the plug it's trying to be set on"
)
for i in range(count):
if plug.isArray:
if value[i] is None:
continue
childPlug = plug.elementByLogicalIndex(i)
else:
childPlug = plug.child(i)
setValueOnPlug(
childPlug,
value[i],
elideLock,
exclusionPredicate,
inclusionPredicate,
faultTolerant,
modifier=modifier,
doIt=False,
)
if doIt:
modifier.doIt()
# @todo: return original values list
return []
if plug.isArray and isinstance(value, dict):
for index, elemVal in value.items():
if not isinstance(index, int):
try:
index = int(index)
except ValueError as e:
if faultTolerant:
return None
raise RuntimeError("Non numerical array index") from e
childPlug = plug.elementByLogicalIndex(index)
setValueOnPlug(
childPlug,
elemVal,
elideLock,
exclusionPredicate,
inclusionPredicate,
faultTolerant,
modifier=modifier,
doIt=False,
)
if doIt:
modifier.doIt()
return []
if plug.isArray:
if faultTolerant:
return None
raise RuntimeError("Unable to set scalar value on array plug")
if plug.isCompound and isinstance(value, dict):
depFn = om2.MFnDependencyNode(plug.node())
childPlugMap = {}
for i in range(plug.numChildren()):
childPlug = plug.child(i)
alias = depFn.plugsAlias(childPlug)
if alias:
childPlugMap[alias] = childPlug
attrFn = om2.MFnAttribute(childPlug.attribute())
childPlugMap[attrFn.shortName] = childPlug
childPlugMap[attrFn.name] = childPlug
childPlugMap[i] = childPlug
childPlugMap[str(i)] = childPlug
for key, childValue in value.items():
if key not in childPlugMap:
if faultTolerant:
return None
raise RuntimeError(f"Child plug {key} not found in plug {plug.name}")
setValueOnPlug(
childPlugMap[key],
childValue,
elideLock,
exclusionPredicate,
inclusionPredicate,
faultTolerant,
modifier=modifier,
doIt=False,
)
if doIt:
modifier.doIt()
return []
def _setValueWith(setter, previousValue, targetValue, extraConversion=lambda x: x):
if previousValue is None or previousValue != targetValue:
setter(plug, extraConversion(targetValue))
if doIt:
modifier.doIt()
return previousValue
logger.debug("Current value for %s already set to %s", plug, targetValue)
if faultTolerant:
return previousValue
return None
# ----------- NUMERIC -----------
if attrType == om2.MFn.kNumericAttribute:
t = fn.numericType()
# For numeric value, we still do explicit coversion below, in case some rig updates
# end in attribute type changes, which will cause an unnecessary exception here.
if t == om2.MFnNumericData.kBoolean:
prevVal = plug.asBool()
return _setValueWith(modifier.newPlugValueBool, prevVal, bool(value))
if __num_as_float(t):
prevVal = plug.asFloat()
return _setValueWith(modifier.newPlugValueFloat, prevVal, float(value))
if __num_as_int(t):
prevVal = plug.asInt()
return _setValueWith(modifier.newPlugValueInt, prevVal, int(value))
if t <= om2.MFnNumericData.kInvalid or t >= om2.MFnNumericData.kLast:
if faultTolerant:
return None
raise RuntimeError("invalid data type")
# @TODO impelement set for multiples
# ------------ UNIT -------------
if attrType == om2.MFn.kUnitAttribute:
t = fn.unitType()
if t == om2.MFnUnitAttribute.kAngle:
if not asDegrees:
prevVal = plug.asMAngle().asRadians()
conversion = lambda x: om2.MAngle(x, om2.MAngle.kRadians)
else:
prevVal = plug.asMAngle().asDegrees()
conversion = lambda x: om2.MAngle(x, om2.MAngle.kDegrees)
return _setValueWith(
modifier.newPlugValueMAngle, prevVal, value, conversion
)
if t == om2.MFnUnitAttribute.kDistance:
prevVal = plug.asDouble()
return _setValueWith(modifier.newPlugValueDouble, prevVal, value)
if t == om2.MFnUnitAttribute.kTime:
prevVal = plug.asMTime().value
return _setValueWith(modifier.newPlugValueMTime, prevVal, value, om2.MTime)
return None
# ----------- MATRIX ------------
if attrType == om2.MFn.kMatrixAttribute:
if hasattr(value, "__iter__"):
mtxVal = om2.MMatrix(value)
elif isinstance(value, (om2.MMatrix, om2.MFloatMatrix)):
mtxVal = om2.MMatrix(value)
else:
if faultTolerant:
# @todo: log warning or something?!
return None
raise TypeError("unsupported value type passed to setValueOnPlug")
prevVal = om2.MFnMatrixData(plug.asMObject()).matrix()
conversion = lambda x: om2.MFnMatrixData().create(x)
return _setValueWith(modifier.newPlugValue, prevVal, mtxVal, conversion)
# ------------ ENUM -------------
if attrType == om2.MFn.kEnumAttribute:
prevVal = plug.asShort()
if isinstance(value, str):
# Find the index from the string value
validNames = plugEnumNames(plug)
if value in validNames:
value = validNames.index(value)
else:
raise ValueError(
f"Invalid enum name '{value}' for plug {plug}, valid enum names are {validNames}."
)
return _setValueWith(modifier.newPlugValueShort, prevVal, value)
# ----------- TYPED -------------
if attrType == om2.MFn.kTypedAttribute:
t = fn.attrType()
if t == om2.MFnData.kString:
prevVal = plug.asString()
return _setValueWith(modifier.newPlugValueString, prevVal, value)
if t == om2.MFnData.kMatrix:
if hasattr(value, "__iter__"):
mtxVal = om2.MMatrix(value)
elif isinstance(value, (om2.MMatrix, om2.MFloatMatrix)):
mtxVal = om2.MMatrix(value)
else:
if faultTolerant:
# @todo: log warning or something?!
return None
raise TypeError("unsupported value type passed to setValueOnPlug")
prevVal = plug.asMDataHandle().asMatrix()
conversion = lambda x: om2.MFnMatrixData().create(x)
return _setValueWith(modifier.newPlugValue, prevVal, mtxVal, conversion)
# @todo return as list for alignment with rest of methods?
# ----- array cases -----
fnData = None
if t == om2.MFnData.kStringArray:
fnData = om2.MFnStringArrayData()
elif t == om2.MFnData.kIntArray:
fnData = om2.MFnIntArrayData()
elif t == om2.MFnData.kDoubleArray:
fnData = om2.MFnIntArrayData()
elif t == om2.MFnData.kVectorArray:
fnData = om2.MFnVectorArrayData()
elif t == om2.MFnData.kPointArray:
fnData = om2.MFnPointArrayData()
elif t == om2.MFnData.kMatrixArray:
fnData = om2.MFnMatrixArrayData()
# @todo: implement value diffing like for all other cases
attrData = fnData.create(value) if fnData is not None else None
if _setValueWith(modifier.newPlugValue, None, attrData):
return [] # @todo: implement proper populated list return for array types
# ----- maya invalid -----
if t == om2.MFnData.kInvalid:
if faultTolerant:
return None
raise RuntimeError(
"invalid plug type, like, literally kInvalid!"
) # @todo: better message and error
# @todo: Message and generic remain unaddressed for now.
# Generic can be tricky and require graph inspection to implement properly.
# Message is kind of unlikely to have a meaningful implementation that
# doesn't simply become some confusing ulterior connect-plug
return None