# 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 . """ 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