# 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 . """ # mirrortable.py import mutils # Example 1: # Create a MirrorTable instance from the given objects mt = mutils.MirrorTable.fromObjects(objects, "_l_", "_r_", MirrorPlane.YZ) # Example 2: # Create a MirrorTable instance from the selected objects objects = maya.cmds.ls(selection=True) mt = mutils.MirrorTable.fromObjects(objects, "_l_", "_r_", MirrorPlane.YZ) # Example 3: # Save the MirrorTable to the given JSON path path = "/tmp/mirrortable.json" mt.save(path) # Example 4: # Create a MirrorTable instance from the given JSON path path = "/tmp/mirrortable.json" mt = mutils.MirrorTable.fromPath(path) # Example 5: # Mirror all the objects from file mt.load() # Example 6: # Mirror only the selected objects objects = maya.cmds.ls(selection=True) or [] mt.load(objects=objects) # Example 7: # Mirror all objects from file to the given namespaces mt.load(namespaces=["character1", "character2"]) # Example 8: # Mirror only the given objects mt.load(objects=["character1:Hand_L", "character1:Finger_L"]) # Example 9: # Mirror all objects from left to right mt.load(option=mutils.MirrorOption.LeftToRight) # Example 10: # Mirror all objects from right to left mt.load(option=mutils.MirrorOption.RightToLeft) # Example 11: # Mirror only the current pose mt.load(animation=False) """ import re import mutils import logging import traceback from studiovendor import six try: import maya.cmds except ImportError: traceback.print_exc() __all__ = [ "MirrorTable", "MirrorPlane", "MirrorOption", "saveMirrorTable", ] logger = logging.getLogger(__name__) RE_LEFT_SIDE = "Left|left|Lf|lt_|_lt|lf_|_lf|_l_|_L|L_|:l_|^l_|_l$|:L|^L" RE_RIGHT_SIDE = "Right|right|Rt|rt_|_rt|_r_|_R|R_|:r_|^r_|_r$|:R|^R" VALID_NODE_TYPES = ["joint", "transform"] class MirrorPlane: YZ = [-1, 1, 1] XZ = [1, -1, 1] XY = [1, 1, -1] class MirrorOption: Swap = 0 LeftToRight = 1 RightToLeft = 2 class KeysOption: All = "All Keys" SelectedRange = "Selected Range" def saveMirrorTable(path, objects, metadata=None, *args, **kwargs): """ Convenience function for saving a mirror table to the given disc location. :type path: str :type objects: list[str] :type metadata: dict or None :type args: list :type kwargs: dict :rtype: MirrorTable """ mirrorTable = MirrorTable.fromObjects(objects, *args, **kwargs) if metadata: mirrorTable.updateMetadata(metadata) mirrorTable.save(path) return mirrorTable class MirrorTable(mutils.TransferObject): @classmethod @mutils.timing @mutils.unifyUndo @mutils.showWaitCursor @mutils.restoreSelection def fromObjects( cls, objects, leftSide=None, rightSide=None, mirrorPlane=None ): """ Create a new Mirror Table instance from the given Maya object/controls. :type objects: list[str] :type leftSide: str :type rightSide: str :type mirrorPlane: mirrortable.MirrorPlane or str :rtype: MirrorTable """ mirrorPlane = mirrorPlane or MirrorPlane.YZ if isinstance(mirrorPlane, six.string_types): if mirrorPlane.lower() == "yz": mirrorPlane = MirrorPlane.YZ elif mirrorPlane.lower() == "xz": mirrorPlane = MirrorPlane.XZ elif mirrorPlane.lower() == "xy": mirrorPlane = MirrorPlane.XY mirrorTable = cls() mirrorTable.setMetadata("left", leftSide) mirrorTable.setMetadata("right", rightSide) mirrorTable.setMetadata("mirrorPlane", mirrorPlane) for obj in objects: nodeType = maya.cmds.nodeType(obj) if nodeType in VALID_NODE_TYPES: mirrorTable.add(obj) else: msg = "Node of type {0} is not supported. Node name: {1}" msg = msg.format(nodeType, obj) logger.info(msg) return mirrorTable @staticmethod def findLeftSide(objects): """ Return the left side naming convention for the given objects :type objects: list[str] :rtype: str """ return MirrorTable.findSide(objects, RE_LEFT_SIDE) @staticmethod def findRightSide(objects): """ Return the right side naming convention for the given objects :type objects: list[str] :rtype: str """ return MirrorTable.findSide(objects, RE_RIGHT_SIDE) @classmethod def findSide(cls, objects, reSides): """ Return the naming convention for the given object names. :type objects: list[str] :type reSides: str or list[str] :rtype: str """ if isinstance(reSides, six.string_types): reSides = reSides.split("|") # Compile the list of regular expressions into a re.object reSides = [re.compile(side) for side in reSides] for obj in objects: obj = obj.split("|")[-1] obj = obj.split(":")[-1] for reSide in reSides: m = reSide.search(obj) if m: side = m.group() if obj.startswith(side): side += "*" if obj.endswith(side): side = "*" + side return side return "" @staticmethod def matchSide(name, side): """ Return True if the name contains the given side. :type name: str :type side: str :rtype: bool """ if side: return MirrorTable.rreplace(name, side, "X") != name return False @staticmethod def rreplace(name, old, new): """ convenience method. Example: print MirrorTable.rreplace("CHR1:RIG:RhandCON", ":R", ":L") "CHR1:RIG:LhandCON" :type name: str :type old: str :type new: str :rtype: str """ names = [] for n in name.split("|"): namespaces = '' if ':' in n: namespaces, n = n.rsplit(':', 1) other = mutils.mirrortable.MirrorTable.replace(n, old, new) if namespaces: other = ':'.join([namespaces, other]) names.append(other) return "|".join(names) @staticmethod def replace(name, old, new): """ A static method that is called by self.mirrorObject. :type name: str :type old: str :type new: str :rtype: str """ # Support for replacing a prefix naming convention. if old.endswith("*") or new.endswith("*"): name = MirrorTable.replacePrefix(name, old, new) # Support for replacing a suffix naming convention. elif old.startswith("*") or new.startswith("*"): name = MirrorTable.replaceSuffix(name, old, new) # Support for all other naming conventions. else: name = name.replace(old, new) return name @staticmethod def replacePrefix(name, old, new): """ Replace the given old prefix with the given new prefix. It should also support long names. # Example: self.replacePrefix("R_footRoll", "R", "L") # Result: "L_footRoll" and not "L_footLoll". self.replacePrefix("Grp|Ch1:R_footExtra|Ch1:R_footRoll", "R_", "L_") # Result: "Grp|Ch1:L_footExtra|Ch1:L_footRoll" :type name: str :type old: str :type new: str :rtype: str """ old = old.replace("*", "") new = new.replace("*", "") if name.startswith(old): name = name.replace(old, new, 1) return name @staticmethod def replaceSuffix(name, old, new): """ Replace the given old suffix with the given new suffix. It should also support long names. # Example: self.replaceSuffix("footRoll_R", "R", "L") # Result: "footRoll_L" and not "footLoll_L". self.replaceSuffix("Grp|Ch1:footExtra_R|Ch1:footRoll_R", "_R", "_L") # Result: "Grp|Ch1:footExtra_L|Ch1:footRoll_L" :type name: str :type old: str :type new: str :rtype: str """ old = old.replace("*", "") new = new.replace("*", "") if name.endswith(old): name = name[:-len(old)] + new return name def mirrorObject(self, obj): """ Return the other/opposite side for the given name. Example: print self.mirrorObject("FKSholder_L") # FKShoulder_R :type obj: str :rtype: str or None """ leftSide = self.leftSide() rightSide = self.rightSide() return self._mirrorObject(obj, leftSide, rightSide) @staticmethod def _mirrorObject(obj, leftSide, rightSide): """ A static method that is called by self.mirrorObject. :type obj: str :rtype: str or None """ dstName = MirrorTable.rreplace(obj, leftSide, rightSide) if obj == dstName: dstName = MirrorTable.rreplace(obj, rightSide, leftSide) if dstName != obj: return dstName # Return None as the given name is probably a center control # and doesn't have an opposite side. return None @staticmethod def animCurve(obj, attr): """ :type obj: str :type attr: str :rtype: str """ fullname = obj + "." + attr connections = maya.cmds.listConnections(fullname, d=False, s=True) if connections: return connections[0] return None @staticmethod def scaleKey(obj, attr, time=None): """ :type obj: str :type attr: str """ curve = MirrorTable.animCurve(obj, attr) if curve: maya.cmds.selectKey(curve) if time: maya.cmds.scaleKey(time=time, iub=False, ts=1, fs=1, vs=-1, vp=0, animation="keys") else: maya.cmds.scaleKey(iub=False, ts=1, fs=1, vs=-1, vp=0, animation="keys") @staticmethod def formatValue(attr, value, mirrorAxis): """ :type attr: str :type value: float :type mirrorAxis: list[int] :rtype: float """ if MirrorTable.isAttrMirrored(attr, mirrorAxis): return value * -1 return value @staticmethod def maxIndex(numbers): """ Finds the largest number in a list :type numbers: list[float] or list[str] :rtype: int """ m = 0 result = 0 for i in numbers: v = abs(float(i)) if v > m: m = v result = numbers.index(i) return result @staticmethod def axisWorldPosition(obj, axis): """ :type obj: str :type axis: list[int] :rtype: list[float] """ transform1 = maya.cmds.createNode("transform", name="transform1") try: transform1, = maya.cmds.parent(transform1, obj, r=True) maya.cmds.setAttr(transform1 + ".t", *axis) maya.cmds.setAttr(transform1 + ".r", 0, 0, 0) maya.cmds.setAttr(transform1 + ".s", 1, 1, 1) return maya.cmds.xform(transform1, q=True, ws=True, piv=True) finally: maya.cmds.delete(transform1) @staticmethod def isAttrMirrored(attr, mirrorAxis): """ :type attr: str :type mirrorAxis: list[int] :rtype: float """ if mirrorAxis == [-1, 1, 1]: if attr == "translateX" or attr == "rotateY" or attr == "rotateZ": return True elif mirrorAxis == [1, -1, 1]: if attr == "translateY" or attr == "rotateX" or attr == "rotateZ": return True elif mirrorAxis == [1, 1, -1]: if attr == "translateZ" or attr == "rotateX" or attr == "rotateY": return True elif mirrorAxis == [-1, -1, -1]: if attr == "translateX" or attr == "translateY" or attr == "translateZ": return True return False @staticmethod def isAxisMirrored(srcObj, dstObj, axis, mirrorPlane): """ :type srcObj: str :type dstObj: str :type axis: list[int] :type mirrorPlane: list[int] :rtype: int """ old1 = maya.cmds.xform(srcObj, q=True, ws=True, piv=True) old2 = maya.cmds.xform(dstObj, q=True, ws=True, piv=True) new1 = MirrorTable.axisWorldPosition(srcObj, axis) new2 = MirrorTable.axisWorldPosition(dstObj, axis) mp = mirrorPlane v1 = mp[0]*(new1[0]-old1[0]), mp[1]*(new1[1]-old1[1]), mp[2]*(new1[2]-old1[2]) v2 = new2[0]-old2[0], new2[1]-old2[1], new2[2]-old2[2] d = sum(p*q for p, q in zip(v1, v2)) if d >= 0.0: return False return True @staticmethod def _calculateMirrorAxis(obj, mirrorPlane): """ :type obj: str :rtype: list[int] """ result = [1, 1, 1] transform0 = maya.cmds.createNode("transform", name="transform0") try: transform0, = maya.cmds.parent(transform0, obj, r=True) transform0, = maya.cmds.parent(transform0, w=True) maya.cmds.setAttr(transform0 + ".t", 0, 0, 0) t1 = MirrorTable.axisWorldPosition(transform0, [1, 0, 0]) t2 = MirrorTable.axisWorldPosition(transform0, [0, 1, 0]) t3 = MirrorTable.axisWorldPosition(transform0, [0, 0, 1]) t1 = "%.3f" % t1[0], "%.3f" % t1[1], "%.3f" % t1[2] t2 = "%.3f" % t2[0], "%.3f" % t2[1], "%.3f" % t2[2] t3 = "%.3f" % t3[0], "%.3f" % t3[1], "%.3f" % t3[2] if mirrorPlane == MirrorPlane.YZ: # [-1, 1, 1]: x = [t1[0], t2[0], t3[0]] i = MirrorTable.maxIndex(x) result[i] = -1 if mirrorPlane == MirrorPlane.XZ: # [1, -1, 1]: y = [t1[1], t2[1], t3[1]] i = MirrorTable.maxIndex(y) result[i] = -1 if mirrorPlane == MirrorPlane.XY: # [1, 1, -1]: z = [t1[2], t2[2], t3[2]] i = MirrorTable.maxIndex(z) result[i] = -1 finally: maya.cmds.delete(transform0) return result def select(self, objects=None, namespaces=None, **kwargs): """ Select the objects contained in 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 leftSide(self): """ :rtype: str | None """ return self.metadata().get("left") def rightSide(self): """ :rtype: str | None """ return self.metadata().get("right") def mirrorPlane(self): """ :rtype: lsit[int] | None """ return self.metadata().get("mirrorPlane") def mirrorAxis(self, name): """ :rtype: list[int] """ return self.objects()[name]["mirrorAxis"] def leftCount(self, objects=None): """ :type objects: list[str] :rtype: int """ if objects is None: objects = self.objects() return len([obj for obj in objects if self.isLeftSide(obj)]) def rightCount(self, objects=None): """ :type objects: list[str] :rtype: int """ if objects is None: objects = self.objects() return len([obj for obj in objects if self.isRightSide(obj)]) def createObjectData(self, name): """ :type name: :rtype: """ result = {"mirrorAxis": self.calculateMirrorAxis(name)} return result def matchObjects( self, objects=None, namespaces=None, ): """ :type objects: list[str] :type namespaces: list[str] :rtype: list[str] """ srcObjects = self.objects().keys() matches = mutils.matchNames( srcObjects=srcObjects, dstObjects=objects, dstNamespaces=namespaces, ) for srcNode, dstNode in matches: dstName = dstNode.name() mirrorAxis = self.mirrorAxis(srcNode.name()) yield srcNode.name(), dstName, mirrorAxis def rightToLeft(self): """""" self.load(option=MirrorOption.RightToLeft) def leftToRight(self): """""" self.load(option=MirrorOption.LeftToRight) @mutils.timing @mutils.unifyUndo @mutils.showWaitCursor @mutils.restoreSelection def load( self, objects=None, namespaces=None, option=None, keysOption=None, time=None, ): """ Load the mirror table for the given objects. :type objects: list[str] :type namespaces: list[str] :type option: mirrorOptions :type keysOption: None or KeysOption.SelectedRange :type time: None or list[int] """ if option and not isinstance(option, int): if option.lower() == "swap": option = 0 elif option.lower() == "left to right": option = 1 elif option.lower() == "right to left": option = 2 else: raise ValueError('Invalid load option=' + str(option)) self.validate(namespaces=namespaces) results = {} animation = True foundObject = False srcObjects = self.objects().keys() if option is None: option = MirrorOption.Swap if keysOption == KeysOption.All: time = None elif keysOption == KeysOption.SelectedRange: time = mutils.selectedFrameRange() # Check to make sure that the given time is not a single frame if time and time[0] == time[1]: time = None animation = False matches = mutils.matchNames( srcObjects=srcObjects, dstObjects=objects, dstNamespaces=namespaces, ) for srcNode, dstNode in matches: dstObj = dstNode.name() dstObj2 = self.mirrorObject(dstObj) or dstObj if dstObj2 not in results: results[dstObj] = dstObj2 mirrorAxis = self.mirrorAxis(srcNode.name()) dstObjExists = maya.cmds.objExists(dstObj) dstObj2Exists = maya.cmds.objExists(dstObj2) if dstObjExists and dstObj2Exists: foundObject = True if animation: self.transferAnimation(dstObj, dstObj2, mirrorAxis=mirrorAxis, option=option, time=time) else: self.transferStatic(dstObj, dstObj2, mirrorAxis=mirrorAxis, option=option) else: if not dstObjExists: msg = "Cannot find destination object {0}" msg = msg.format(dstObj) logger.debug(msg) if not dstObj2Exists: msg = "Cannot find mirrored destination object {0}" msg = msg.format(dstObj2) logger.debug(msg) # Return the focus to the Maya window maya.cmds.setFocus("MayaWindow") if not foundObject: text = "No objects match when loading data. " \ "Turn on debug mode to see more details." raise mutils.NoMatchFoundError(text) def transferStatic(self, srcObj, dstObj, mirrorAxis=None, attrs=None, option=MirrorOption.Swap): """ :type srcObj: str :type dstObj: str :type mirrorAxis: list[int] :type attrs: None | list[str] :type option: MirrorOption """ srcValue = None dstValue = None srcValid = self.isValidMirror(srcObj, option) dstValid = self.isValidMirror(dstObj, option) if attrs is None: attrs = maya.cmds.listAttr(srcObj, keyable=True) or [] for attr in attrs: dstAttr = dstObj + "." + attr srcAttr = srcObj + "." + attr if maya.cmds.objExists(dstAttr): if dstValid: srcValue = maya.cmds.getAttr(srcAttr) if srcValid: dstValue = maya.cmds.getAttr(dstAttr) if dstValid: self.setAttr(dstObj, attr, srcValue, mirrorAxis=mirrorAxis) if srcValid: self.setAttr(srcObj, attr, dstValue, mirrorAxis=mirrorAxis) else: logger.debug("Cannot find destination attribute %s" % dstAttr) def setAttr(self, name, attr, value, mirrorAxis=None): """ :type name: str :type: attr: str :type: value: int | float :type mirrorAxis: Axis or None """ if mirrorAxis is not None: value = self.formatValue(attr, value, mirrorAxis) try: maya.cmds.setAttr(name + "." + attr, value) except RuntimeError: msg = "Cannot mirror static attribute {name}.{attr}" msg = msg.format(name=name, attr=attr) logger.debug(msg) def transferAnimation(self, srcObj, dstObj, mirrorAxis=None, option=MirrorOption.Swap, time=None): """ :type srcObj: str :type dstObj: str :type mirrorAxis: Axis or None """ srcValid = self.isValidMirror(srcObj, option) dstValid = self.isValidMirror(dstObj, option) tmpObj, = maya.cmds.duplicate(srcObj, name='DELETE_ME', parentOnly=True) try: if dstValid: self._transferAnimation(srcObj, tmpObj, time=time) if srcValid: self._transferAnimation(dstObj, srcObj, mirrorAxis=mirrorAxis, time=time) if dstValid: self._transferAnimation(tmpObj, dstObj, mirrorAxis=mirrorAxis, time=time) finally: maya.cmds.delete(tmpObj) def _transferAnimation(self, srcObj, dstObj, attrs=None, mirrorAxis=None, time=None): """ :type srcObj: str :type dstObj: str :type attrs: list[str] :type time: list[int] :type mirrorAxis: list[int] """ maya.cmds.cutKey(dstObj, time=time or ()) # remove keys if maya.cmds.copyKey(srcObj, time=time or ()): if not time: maya.cmds.pasteKey(dstObj, option="replaceCompletely") else: maya.cmds.pasteKey(dstObj, time=time, option="replace") if attrs is None: attrs = maya.cmds.listAttr(srcObj, keyable=True) or [] for attr in attrs: srcAttribute = mutils.Attribute(srcObj, attr) dstAttribute = mutils.Attribute(dstObj, attr) if dstAttribute.exists(): if dstAttribute.isConnected(): if self.isAttrMirrored(attr, mirrorAxis): maya.cmds.scaleKey(dstAttribute.name(), valueScale=-1, attribute=attr) else: value = srcAttribute.value() self.setAttr(dstObj, attr, value, mirrorAxis) def isValidMirror(self, obj, option): """ :type obj: str :type option: MirrorOption :rtype: bool """ if option == MirrorOption.Swap: return True elif option == MirrorOption.LeftToRight and self.isLeftSide(obj): return False elif option == MirrorOption.RightToLeft and self.isRightSide(obj): return False else: return True def isLeftSide(self, name): """ Return True if the object contains the left side string. # Group|Character1:footRollExtra_L|Character1:footRoll_L # Group|footRollExtra_L|footRoll_LShape # footRoll_L # footRoll_LShape :type name: str :rtype: bool """ side = self.leftSide() return self.matchSide(name, side) def isRightSide(self, name): """ Return True if the object contains the right side string. # Group|Character1:footRollExtra_R|Character1:footRoll_R # Group|footRollExtra_R|footRoll_RShape # footRoll_R # footRoll_RShape :type name: str :rtype: bool """ side = self.rightSide() return self.matchSide(name, side) def calculateMirrorAxis(self, srcObj): """ :type srcObj: str :rtype: list[int] """ result = [1, 1, 1] dstObj = self.mirrorObject(srcObj) or srcObj mirrorPlane = self.mirrorPlane() if dstObj == srcObj or not maya.cmds.objExists(dstObj): # The source object does not have an opposite side result = MirrorTable._calculateMirrorAxis(srcObj, mirrorPlane) else: # The source object does have an opposite side if MirrorTable.isAxisMirrored(srcObj, dstObj, [1, 0, 0], mirrorPlane): result[0] = -1 if MirrorTable.isAxisMirrored(srcObj, dstObj, [0, 1, 0], mirrorPlane): result[1] = -1 if MirrorTable.isAxisMirrored(srcObj, dstObj, [0, 0, 1], mirrorPlane): result[2] = -1 return result