Update
This commit is contained in:
565
2023/scripts/animation_tools/studiolibrary/mutils/attribute.py
Normal file
565
2023/scripts/animation_tools/studiolibrary/mutils/attribute.py
Normal file
@@ -0,0 +1,565 @@
|
||||
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
|
||||
#
|
||||
# This library is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version. This library is distributed in the
|
||||
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
|
||||
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
# See the GNU Lesser General Public License for more details.
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
Example:
|
||||
|
||||
import mutils
|
||||
|
||||
attr = mutils.Attribute("sphere1", "translateX")
|
||||
attr.set(100)
|
||||
"""
|
||||
import logging
|
||||
from studiovendor import six
|
||||
|
||||
try:
|
||||
import maya.cmds
|
||||
except ImportError:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
VALID_CONNECTIONS = [
|
||||
"animCurve",
|
||||
"animBlend",
|
||||
"pairBlend",
|
||||
"character"
|
||||
]
|
||||
|
||||
VALID_BLEND_ATTRIBUTES = [
|
||||
"int",
|
||||
"long",
|
||||
"float",
|
||||
"short",
|
||||
"double",
|
||||
"doubleAngle",
|
||||
"doubleLinear",
|
||||
]
|
||||
|
||||
VALID_ATTRIBUTE_TYPES = [
|
||||
"int",
|
||||
"long",
|
||||
"enum",
|
||||
"bool",
|
||||
"string",
|
||||
"float",
|
||||
"short",
|
||||
"double",
|
||||
"doubleAngle",
|
||||
"doubleLinear",
|
||||
]
|
||||
|
||||
|
||||
class AttributeError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
pass
|
||||
|
||||
|
||||
class Attribute(object):
|
||||
|
||||
|
||||
@classmethod
|
||||
def listAttr(cls, name, **kwargs):
|
||||
"""
|
||||
Return a list of Attribute from the given name and matching criteria.
|
||||
|
||||
If no flags are specified all attributes are listed.
|
||||
|
||||
:rtype: list[Attribute]
|
||||
"""
|
||||
attrs = maya.cmds.listAttr(name, **kwargs) or []
|
||||
return [cls(name, attr) for attr in attrs]
|
||||
|
||||
@classmethod
|
||||
def deleteProxyAttrs(cls, name):
|
||||
"""Delete all the proxy attributes for the given object name."""
|
||||
attrs = cls.listAttr(name, unlocked=True, keyable=True) or []
|
||||
for attr in attrs:
|
||||
if attr.isProxy():
|
||||
attr.delete()
|
||||
|
||||
def __init__(self, name, attr=None, value=None, type=None, cache=True):
|
||||
"""
|
||||
:type name: str
|
||||
:type attr: str | None
|
||||
:type type: str | None
|
||||
:type value: object | None
|
||||
:type cache: bool
|
||||
"""
|
||||
if "." in name:
|
||||
name, attr = name.split(".")
|
||||
|
||||
if attr is None:
|
||||
msg = "Cannot initialise attribute instance without a given attr."
|
||||
raise AttributeError(msg)
|
||||
|
||||
self._name = six.text_type(name)
|
||||
self._attr = six.text_type(attr)
|
||||
self._type = type
|
||||
self._value = value
|
||||
self._cache = cache
|
||||
self._fullname = None
|
||||
|
||||
def __str__(self):
|
||||
"""
|
||||
:rtype: str
|
||||
"""
|
||||
return str(self.toDict())
|
||||
|
||||
def name(self):
|
||||
"""
|
||||
Return the Maya object name for the attribute.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
return self._name
|
||||
|
||||
def attr(self):
|
||||
"""
|
||||
Return the attribute name.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
return self._attr
|
||||
|
||||
def isLocked(self):
|
||||
"""
|
||||
Return true if the attribute is locked.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return maya.cmds.getAttr(self.fullname(), lock=True)
|
||||
|
||||
def isUnlocked(self):
|
||||
"""
|
||||
Return true if the attribute is unlocked.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return not self.isLocked()
|
||||
|
||||
def toDict(self):
|
||||
"""
|
||||
Return a dictionary of the attribute object.
|
||||
|
||||
:rtype: dict
|
||||
"""
|
||||
result = {
|
||||
"type": self.type(),
|
||||
"value": self.value(),
|
||||
"fullname": self.fullname(),
|
||||
}
|
||||
return result
|
||||
|
||||
def isValid(self):
|
||||
"""
|
||||
Return true if the attribute type is valid.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self.type() in VALID_ATTRIBUTE_TYPES
|
||||
|
||||
def isProxy(self):
|
||||
"""
|
||||
Return true if the attribute is a proxy
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
if not maya.cmds.addAttr(self.fullname(), query=True, exists=True):
|
||||
return False
|
||||
|
||||
return maya.cmds.addAttr(self.fullname(), query=True, usedAsProxy=True)
|
||||
|
||||
def delete(self):
|
||||
"""Delete the attribute"""
|
||||
maya.cmds.deleteAttr(self.fullname())
|
||||
|
||||
def exists(self):
|
||||
"""
|
||||
Return true if the object and attribute exists in the scene.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return maya.cmds.objExists(self.fullname())
|
||||
|
||||
def prettyPrint(self):
|
||||
"""
|
||||
Print the command for setting the attribute value
|
||||
"""
|
||||
msg = 'maya.cmds.setAttr("{0}", {1})'
|
||||
msg = msg.format(self.fullname(), self.value())
|
||||
print(msg)
|
||||
|
||||
def clearCache(self):
|
||||
"""
|
||||
Clear all cached values
|
||||
"""
|
||||
self._type = None
|
||||
self._value = None
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
This method will be deprecated.
|
||||
"""
|
||||
self.clearCache()
|
||||
|
||||
def query(self, **kwargs):
|
||||
"""
|
||||
Convenience method for Maya's attribute query command
|
||||
|
||||
:rtype: object
|
||||
"""
|
||||
return maya.cmds.attributeQuery(self.attr(), node=self.name(), **kwargs)
|
||||
|
||||
def listConnections(self, **kwargs):
|
||||
"""
|
||||
Convenience method for Maya's list connections command
|
||||
|
||||
:rtype: list[str]
|
||||
"""
|
||||
return maya.cmds.listConnections(self.fullname(), **kwargs)
|
||||
|
||||
def sourceConnection(self, **kwargs):
|
||||
"""
|
||||
Return the source connection for this attribute.
|
||||
|
||||
:rtype: str | None
|
||||
"""
|
||||
try:
|
||||
return self.listConnections(destination=False, **kwargs)[0]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
def fullname(self):
|
||||
"""
|
||||
Return the name with the attr name.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._fullname is None:
|
||||
self._fullname = '{0}.{1}'.format(self.name(), self.attr())
|
||||
return self._fullname
|
||||
|
||||
def value(self):
|
||||
"""
|
||||
Return the value of the attribute.
|
||||
|
||||
:rtype: float | str | list
|
||||
"""
|
||||
if self._value is None or not self._cache:
|
||||
|
||||
try:
|
||||
self._value = maya.cmds.getAttr(self.fullname())
|
||||
except Exception:
|
||||
msg = 'Cannot GET attribute VALUE for "{0}"'
|
||||
msg = msg.format(self.fullname())
|
||||
logger.exception(msg)
|
||||
|
||||
return self._value
|
||||
|
||||
def type(self):
|
||||
"""
|
||||
Return the type of data currently in the attribute.
|
||||
|
||||
:rtype: str
|
||||
"""
|
||||
if self._type is None:
|
||||
|
||||
try:
|
||||
self._type = maya.cmds.getAttr(self.fullname(), type=True)
|
||||
self._type = six.text_type(self._type)
|
||||
except Exception:
|
||||
msg = 'Cannot GET attribute TYPE for "{0}"'
|
||||
msg = msg.format(self.fullname())
|
||||
logger.exception(msg)
|
||||
|
||||
return self._type
|
||||
|
||||
def set(self, value, blend=100, key=False, clamp=True, additive=False):
|
||||
"""
|
||||
Set the value for the attribute.
|
||||
|
||||
:type key: bool
|
||||
:type clamp: bool
|
||||
:type value: float | str | list
|
||||
:type blend: float
|
||||
"""
|
||||
try:
|
||||
if additive and self.type() != 'bool':
|
||||
if self.attr().startswith('scale'):
|
||||
value = self.value() * (1 + (value - 1) * (blend/100.0))
|
||||
else:
|
||||
value = self.value() + value * (blend/100.0)
|
||||
elif int(blend) == 0:
|
||||
value = self.value()
|
||||
else:
|
||||
_value = (value - self.value()) * (blend/100.00)
|
||||
value = self.value() + _value
|
||||
except TypeError as error:
|
||||
msg = 'Cannot BLEND or ADD attribute {0}: Error: {1}'
|
||||
msg = msg.format(self.fullname(), error)
|
||||
logger.debug(msg)
|
||||
|
||||
try:
|
||||
if self.type() in ["string"]:
|
||||
maya.cmds.setAttr(self.fullname(), value, type=self.type())
|
||||
elif self.type() in ["list", "matrix"]:
|
||||
maya.cmds.setAttr(self.fullname(), *value, type=self.type())
|
||||
else:
|
||||
maya.cmds.setAttr(self.fullname(), value, clamp=clamp)
|
||||
except (ValueError, RuntimeError) as error:
|
||||
msg = "Cannot SET attribute {0}: Error: {1}"
|
||||
msg = msg.format(self.fullname(), error)
|
||||
logger.debug(msg)
|
||||
|
||||
try:
|
||||
if key:
|
||||
self.setKeyframe(value=value)
|
||||
except TypeError as error:
|
||||
msg = 'Cannot KEY attribute {0}: Error: {1}'
|
||||
msg = msg.format(self.fullname(), error)
|
||||
logger.debug(msg)
|
||||
|
||||
def setKeyframe(self, value, respectKeyable=True, **kwargs):
|
||||
"""
|
||||
Set a keyframe with the given value.
|
||||
|
||||
:value: object
|
||||
:respectKeyable: bool
|
||||
:rtype: None
|
||||
"""
|
||||
if self.query(minExists=True):
|
||||
minimum = self.query(minimum=True)[0]
|
||||
if value < minimum:
|
||||
value = minimum
|
||||
|
||||
if self.query(maxExists=True):
|
||||
maximum = self.query(maximum=True)[0]
|
||||
if value > maximum:
|
||||
value = maximum
|
||||
|
||||
kwargs.setdefault("value", value)
|
||||
kwargs.setdefault("respectKeyable", respectKeyable)
|
||||
|
||||
maya.cmds.setKeyframe(self.fullname(), **kwargs)
|
||||
|
||||
def setStaticKeyframe(self, value, time, option):
|
||||
"""
|
||||
Set a static keyframe at the given time.
|
||||
|
||||
:type value: object
|
||||
:type time: (int, int)
|
||||
:type option: PasteOption
|
||||
"""
|
||||
if option == "replaceCompletely":
|
||||
maya.cmds.cutKey(self.fullname())
|
||||
self.set(value, key=False)
|
||||
|
||||
# This should be changed to only look for animation.
|
||||
# Also will need to support animation layers ect...
|
||||
elif self.isConnected():
|
||||
|
||||
# TODO: Should also support static attrs when there is animation.
|
||||
if option == "replace":
|
||||
maya.cmds.cutKey(self.fullname(), time=time)
|
||||
self.insertStaticKeyframe(value, time)
|
||||
|
||||
elif option == "replace":
|
||||
self.insertStaticKeyframe(value, time)
|
||||
|
||||
else:
|
||||
self.set(value, key=False)
|
||||
|
||||
def insertStaticKeyframe(self, value, time):
|
||||
"""
|
||||
Insert a static keyframe at the given time with the given value.
|
||||
|
||||
:type value: float | str
|
||||
:type time: (int, int)
|
||||
:rtype: None
|
||||
"""
|
||||
startTime, endTime = time
|
||||
duration = endTime - startTime
|
||||
try:
|
||||
# Offset all keyframes from the start position.
|
||||
maya.cmds.keyframe(self.fullname(), relative=True, time=(startTime, 1000000), timeChange=duration)
|
||||
|
||||
# Set a key at the given start and end time
|
||||
self.setKeyframe(value, time=(startTime, startTime), ott='step')
|
||||
self.setKeyframe(value, time=(endTime, endTime), itt='flat', ott='flat')
|
||||
|
||||
# Set the tangent for the next keyframe to flat
|
||||
nextFrame = maya.cmds.findKeyframe(self.fullname(), time=(endTime, endTime), which='next')
|
||||
maya.cmds.keyTangent(self.fullname(), time=(nextFrame, nextFrame), itt='flat')
|
||||
except TypeError as error:
|
||||
msg = "Cannot insert static key frame for attribute {0}: Error: {1}"
|
||||
msg = msg.format(self.fullname(), error)
|
||||
logger.debug(msg)
|
||||
|
||||
def setAnimCurve(self, curve, time, option, source=None, connect=False):
|
||||
"""
|
||||
Set/Paste the give animation curve to this attribute.
|
||||
|
||||
:type curve: str
|
||||
:type option: PasteOption
|
||||
:type time: (int, int)
|
||||
:type source: (int, int)
|
||||
:type connect: bool
|
||||
:rtype: None
|
||||
"""
|
||||
fullname = self.fullname()
|
||||
startTime, endTime = time
|
||||
|
||||
if self.isProxy():
|
||||
logger.debug("Cannot set anim curve for proxy attribute")
|
||||
return
|
||||
|
||||
if not self.exists():
|
||||
logger.debug("Attr does not exists")
|
||||
return
|
||||
|
||||
if self.isLocked():
|
||||
logger.debug("Cannot set anim curve when the attr locked")
|
||||
return
|
||||
|
||||
if source is None:
|
||||
first = maya.cmds.findKeyframe(curve, which='first')
|
||||
last = maya.cmds.findKeyframe(curve, which='last')
|
||||
source = (first, last)
|
||||
|
||||
# We run the copy key command twice to check we have a valid curve.
|
||||
# It needs to run before the cutKey command, otherwise if it fails
|
||||
# we have cut keys for no reason!
|
||||
success = maya.cmds.copyKey(curve, time=source)
|
||||
if not success:
|
||||
msg = "Cannot copy keys from the anim curve {0}"
|
||||
msg = msg.format(curve)
|
||||
logger.debug(msg)
|
||||
return
|
||||
|
||||
if option == "replace":
|
||||
maya.cmds.cutKey(fullname, time=time)
|
||||
else:
|
||||
time = (startTime, startTime)
|
||||
|
||||
try:
|
||||
# Update the clip board with the give animation curve
|
||||
maya.cmds.copyKey(curve, time=source)
|
||||
|
||||
# Note: If the attribute is connected to referenced
|
||||
# animation then the following command will not work.
|
||||
maya.cmds.pasteKey(fullname, option=option, time=time, connect=connect)
|
||||
|
||||
if option == "replaceCompletely":
|
||||
|
||||
# The pasteKey cmd doesn't support all anim attributes
|
||||
# so we need to manually set theses.
|
||||
dstCurve = self.animCurve()
|
||||
if dstCurve:
|
||||
curveColor = maya.cmds.getAttr(curve + ".curveColor")
|
||||
preInfinity = maya.cmds.getAttr(curve + ".preInfinity")
|
||||
postInfinity = maya.cmds.getAttr(curve + ".postInfinity")
|
||||
useCurveColor = maya.cmds.getAttr(curve + ".useCurveColor")
|
||||
|
||||
maya.cmds.setAttr(dstCurve + ".preInfinity", preInfinity)
|
||||
maya.cmds.setAttr(dstCurve + ".postInfinity", postInfinity)
|
||||
maya.cmds.setAttr(dstCurve + ".curveColor", *curveColor[0])
|
||||
maya.cmds.setAttr(dstCurve + ".useCurveColor", useCurveColor)
|
||||
|
||||
except RuntimeError:
|
||||
msg = 'Cannot paste anim curve "{0}" to attribute "{1}"'
|
||||
msg = msg.format(curve, fullname)
|
||||
logger.exception(msg)
|
||||
|
||||
def animCurve(self):
|
||||
"""
|
||||
Return the connected animation curve.
|
||||
|
||||
:rtype: str | None
|
||||
"""
|
||||
result = None
|
||||
|
||||
if self.exists():
|
||||
|
||||
n = self.listConnections(plugs=True, destination=False)
|
||||
|
||||
if n and "animCurve" in maya.cmds.nodeType(n):
|
||||
result = n
|
||||
|
||||
elif n and "character" in maya.cmds.nodeType(n):
|
||||
n = maya.cmds.listConnections(n, plugs=True,
|
||||
destination=False)
|
||||
if n and "animCurve" in maya.cmds.nodeType(n):
|
||||
result = n
|
||||
|
||||
if result:
|
||||
return result[0].split(".")[0]
|
||||
|
||||
def isConnected(self, ignoreConnections=None):
|
||||
"""
|
||||
Return true if the attribute is connected.
|
||||
|
||||
:type ignoreConnections: list[str]
|
||||
:rtype: bool
|
||||
"""
|
||||
if ignoreConnections is None:
|
||||
ignoreConnections = []
|
||||
try:
|
||||
connection = self.listConnections(destination=False)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
if connection:
|
||||
if ignoreConnections:
|
||||
connectionType = maya.cmds.nodeType(connection)
|
||||
for ignoreType in ignoreConnections:
|
||||
if connectionType.startswith(ignoreType):
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def isBlendable(self):
|
||||
"""
|
||||
Return true if the attribute can be blended.
|
||||
|
||||
:rtype: bool
|
||||
"""
|
||||
return self.type() in VALID_BLEND_ATTRIBUTES
|
||||
|
||||
def isSettable(self, validConnections=None):
|
||||
"""
|
||||
Return true if the attribute can be set.
|
||||
|
||||
:type validConnections: list[str]
|
||||
:rtype: bool
|
||||
"""
|
||||
if validConnections is None:
|
||||
validConnections = VALID_CONNECTIONS
|
||||
|
||||
if not self.exists():
|
||||
return False
|
||||
|
||||
if not maya.cmds.listAttr(self.fullname(), unlocked=True, keyable=True, multi=True, scalar=True):
|
||||
return False
|
||||
|
||||
connection = self.listConnections(destination=False)
|
||||
if connection:
|
||||
connectionType = maya.cmds.nodeType(connection)
|
||||
for validType in validConnections:
|
||||
if connectionType.startswith(validType):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
Reference in New Issue
Block a user