# 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 . """ # # pose.py import mutils # Example 1: # Save and load a pose from the selected objects objects = maya.cmds.ls(selection=True) mutils.savePose("/tmp/pose.json", objects) mutils.loadPose("/tmp/pose.json") # Example 2: # Create a pose object from a list of object names pose = mutils.Pose.fromObjects(objects) # Example 3: # Create a pose object from the selected objects objects = maya.cmds.ls(selection=True) pose = mutils.Pose.fromObjects(objects) # Example 4: # Save the pose object to disc path = "/tmp/pose.json" pose.save(path) # Example 5: # Create a pose object from disc path = "/tmp/pose.json" pose = mutils.Pose.fromPath(path) # Load the pose on to the objects from file pose.load() # Load the pose to the selected objects objects = maya.cmds.ls(selection=True) pose.load(objects=objects) # Load the pose to the specified namespaces pose.load(namespaces=["character1", "character2"]) # Load the pose to the specified objects pose.load(objects=["Character1:Hand_L", "Character1:Finger_L"]) """ import logging import mutils try: import maya.cmds except ImportError: import traceback traceback.print_exc() __all__ = ["Pose", "savePose", "loadPose"] logger = logging.getLogger(__name__) _pose_ = None def savePose(path, objects, metadata=None): """ Convenience function for saving a pose to disc for the given objects. Example: path = "C:/example.pose" pose = savePose(path, metadata={'description': 'Example pose'}) print(pose.metadata()) # { 'user': 'Hovel', 'mayaVersion': '2016', 'description': 'Example pose' } :type path: str :type objects: list[str] :type metadata: dict or None :rtype: Pose """ pose = mutils.Pose.fromObjects(objects) if metadata: pose.updateMetadata(metadata) pose.save(path) return pose def loadPose(path, *args, **kwargs): """ Convenience function for loading the given pose path. :type path: str :type args: list :type kwargs: dict :rtype: Pose """ global _pose_ clearCache = kwargs.get("clearCache") if not _pose_ or _pose_.path() != path or clearCache: _pose_ = Pose.fromPath(path) _pose_.load(*args, **kwargs) return _pose_ class Pose(mutils.TransferObject): def __init__(self): mutils.TransferObject.__init__(self) self._cache = None self._mtime = None self._cacheKey = None self._isLoading = False self._selection = None self._mirrorTable = None self._autoKeyFrame = None def createObjectData(self, name): """ Create the object data for the given object name. :type name: str :rtype: dict """ attrs = maya.cmds.listAttr(name, unlocked=True, keyable=True) or [] attrs = list(set(attrs)) attrs = [mutils.Attribute(name, attr) for attr in attrs] data = {"attrs": self.attrs(name)} for attr in attrs: if attr.isValid(): if attr.value() is None: msg = "Cannot save the attribute %s with value None." logger.warning(msg, attr.fullname()) else: data["attrs"][attr.attr()] = { "type": attr.type(), "value": attr.value() } return data def select(self, objects=None, namespaces=None, **kwargs): """ Select the objects contained in the pose file. :type objects: list[str] or None :type namespaces: list[str] or None :rtype: None """ selectionSet = mutils.SelectionSet.fromPath(self.path()) selectionSet.load(objects=objects, namespaces=namespaces, **kwargs) def cache(self): """ Return the current cached attributes for the pose. :rtype: list[(Attribute, Attribute)] """ return self._cache def attrs(self, name): """ Return the attribute for the given name. :type name: str :rtype: dict """ return self.object(name).get("attrs", {}) def attr(self, name, attr): """ Return the attribute data for the given name and attribute. :type name: str :type attr: str :rtype: dict """ return self.attrs(name).get(attr, {}) def attrType(self, name, attr): """ Return the attribute type for the given name and attribute. :type name: str :type attr: str :rtype: str """ return self.attr(name, attr).get("type", None) def attrValue(self, name, attr): """ Return the attribute value for the given name and attribute. :type name: str :type attr: str :rtype: str | int | float """ return self.attr(name, attr).get("value", None) def setMirrorAxis(self, name, mirrorAxis): """ Set the mirror axis for the given name. :type name: str :type mirrorAxis: list[int] """ if name in self.objects(): self.object(name).setdefault("mirrorAxis", mirrorAxis) else: msg = "Object does not exist in pose. " \ "Cannot set mirror axis for %s" logger.debug(msg, name) def mirrorAxis(self, name): """ Return the mirror axis for the given name. :rtype: list[int] | None """ result = None if name in self.objects(): result = self.object(name).get("mirrorAxis", None) if result is None: logger.debug("Cannot find mirror axis in pose for %s", name) return result def updateMirrorAxis(self, name, mirrorAxis): """ Update the mirror axis for the given object name. :type name: str :type mirrorAxis: list[int] """ self.setMirrorAxis(name, mirrorAxis) def mirrorTable(self): """ Return the Mirror Table for the pose. :rtype: mutils.MirrorTable """ return self._mirrorTable def setMirrorTable(self, mirrorTable): """ Set the Mirror Table for the pose. :type mirrorTable: mutils.MirrorTable """ objects = self.objects().keys() self._mirrorTable = mirrorTable for srcName, dstName, mirrorAxis in mirrorTable.matchObjects(objects): self.updateMirrorAxis(dstName, mirrorAxis) def mirrorValue(self, name, attr, mirrorAxis): """ Return the mirror value for the given name, attribute and mirror axis. :type name: str :type attr: str :type mirrorAxis: list[] :rtype: None | int | float """ value = None if self.mirrorTable() and name: value = self.attrValue(name, attr) if value is not None: value = self.mirrorTable().formatValue(attr, value, mirrorAxis) else: logger.debug("Cannot find mirror value for %s.%s", name, attr) return value def beforeLoad(self, clearSelection=True): """ Called before loading the pose. :type clearSelection: bool """ logger.debug('Before Load "%s"', self.path()) if not self._isLoading: maya.cmds.refresh(cv=True) self._isLoading = True maya.cmds.undoInfo(openChunk=True) self._selection = maya.cmds.ls(selection=True) or [] self._autoKeyFrame = maya.cmds.autoKeyframe(query=True, state=True) maya.cmds.autoKeyframe(edit=True, state=False) maya.cmds.select(clear=clearSelection) def afterLoad(self): """Called after loading the pose.""" if not self._isLoading: return logger.debug("After Load '%s'", self.path()) self._isLoading = False if self._selection: maya.cmds.select(self._selection) self._selection = None maya.cmds.autoKeyframe(edit=True, state=self._autoKeyFrame) maya.cmds.undoInfo(closeChunk=True) logger.debug('Loaded "%s"', self.path()) @mutils.timing def load( self, objects=None, namespaces=None, attrs=None, blend=100, key=False, mirror=False, additive=False, refresh=False, batchMode=False, clearCache=False, mirrorTable=None, onlyConnected=False, clearSelection=False, ignoreConnected=False, searchAndReplace=None, ): """ Load the pose to the given objects or namespaces. :type objects: list[str] :type namespaces: list[str] :type attrs: list[str] :type blend: float :type key: bool :type refresh: bool :type mirror: bool :type additive: bool :type mirrorTable: mutils.MirrorTable :type batchMode: bool :type clearCache: bool :type ignoreConnected: bool :type onlyConnected: bool :type clearSelection: bool :type searchAndReplace: (str, str) or None """ if mirror and not mirrorTable: logger.warning("Cannot mirror pose without a mirror table!") mirror = False if batchMode: key = False self.updateCache( objects=objects, namespaces=namespaces, attrs=attrs, batchMode=batchMode, clearCache=clearCache, mirrorTable=mirrorTable, onlyConnected=onlyConnected, ignoreConnected=ignoreConnected, searchAndReplace=searchAndReplace, ) self.beforeLoad(clearSelection=clearSelection) try: self.loadCache(blend=blend, key=key, mirror=mirror, additive=additive) finally: if not batchMode: self.afterLoad() # Return the focus to the Maya window maya.cmds.setFocus("MayaWindow") maya.cmds.refresh(cv=True) def updateCache( self, objects=None, namespaces=None, attrs=None, ignoreConnected=False, onlyConnected=False, mirrorTable=None, batchMode=False, clearCache=True, searchAndReplace=None, ): """ Update the pose cache. :type objects: list[str] or None :type namespaces: list[str] or None :type attrs: list[str] or None :type ignoreConnected: bool :type onlyConnected: bool :type clearCache: bool :type batchMode: bool :type mirrorTable: mutils.MirrorTable :type searchAndReplace: (str, str) or None """ if clearCache or not batchMode or not self._mtime: self._mtime = self.mtime() mtime = self._mtime cacheKey = \ str(mtime) + \ str(objects) + \ str(attrs) + \ str(namespaces) + \ str(ignoreConnected) + \ str(searchAndReplace) + \ str(maya.cmds.currentTime(query=True)) if self._cacheKey != cacheKey or clearCache: self.validate(namespaces=namespaces) self._cache = [] self._cacheKey = cacheKey dstObjects = objects srcObjects = self.objects() usingNamespaces = not objects and namespaces if mirrorTable: self.setMirrorTable(mirrorTable) search = None replace = None if searchAndReplace: search = searchAndReplace[0] replace = searchAndReplace[1] matches = mutils.matchNames( srcObjects, dstObjects=dstObjects, dstNamespaces=namespaces, search=search, replace=replace, ) for srcNode, dstNode in matches: self.cacheNode( srcNode, dstNode, attrs=attrs, onlyConnected=onlyConnected, ignoreConnected=ignoreConnected, usingNamespaces=usingNamespaces, ) if not self.cache(): text = "No objects match when loading data. " \ "Turn on debug mode to see more details." raise mutils.NoMatchFoundError(text) def cacheNode( self, srcNode, dstNode, attrs=None, ignoreConnected=None, onlyConnected=None, usingNamespaces=None ): """ Cache the given pair of nodes. :type srcNode: mutils.Node :type dstNode: mutils.Node :type attrs: list[str] or None :type ignoreConnected: bool or None :type onlyConnected: bool or None :type usingNamespaces: none or list[str] """ mirrorAxis = None mirrorObject = None # Remove the first pipe in-case the object has a parent dstNode.stripFirstPipe() srcName = srcNode.name() if self.mirrorTable(): mirrorObject = self.mirrorTable().mirrorObject(srcName) if not mirrorObject: mirrorObject = srcName msg = "Cannot find mirror object in pose for %s" logger.debug(msg, srcName) # Check if a mirror axis exists for the mirrorObject otherwise # check the srcNode mirrorAxis = self.mirrorAxis(mirrorObject) or self.mirrorAxis(srcName) if mirrorObject and not maya.cmds.objExists(mirrorObject): msg = "Mirror object does not exist in the scene %s" logger.debug(msg, mirrorObject) if usingNamespaces: # Try and use the short name. # Much faster than the long name when setting attributes. try: dstNode = dstNode.toShortName() except mutils.NoObjectFoundError as msg: logger.debug(msg) return except mutils.MoreThanOneObjectFoundError as msg: logger.debug(msg) for attr in self.attrs(srcName): if attrs and attr not in attrs: continue dstAttribute = mutils.Attribute(dstNode.name(), attr) isConnected = dstAttribute.isConnected() if (ignoreConnected and isConnected) or (onlyConnected and not isConnected): continue type_ = self.attrType(srcName, attr) value = self.attrValue(srcName, attr) srcMirrorValue = self.mirrorValue(mirrorObject, attr, mirrorAxis=mirrorAxis) srcAttribute = mutils.Attribute(dstNode.name(), attr, value=value, type=type_) dstAttribute.update() self._cache.append((srcAttribute, dstAttribute, srcMirrorValue)) def loadCache(self, blend=100, key=False, mirror=False, additive=False): """ Load the pose from the current cache. :type blend: float :type key: bool :type mirror: bool :rtype: None """ cache = self.cache() for i in range(0, len(cache)): srcAttribute, dstAttribute, srcMirrorValue = cache[i] if srcAttribute and dstAttribute: if mirror and srcMirrorValue is not None: value = srcMirrorValue else: value = srcAttribute.value() try: dstAttribute.set(value, blend=blend, key=key, additive=additive) except (ValueError, RuntimeError): cache[i] = (None, None) logger.debug('Ignoring %s', dstAttribute.fullname())