# Stdlib imports import re import traceback from functools import partial # Maya imports from maya import cmds import pymel.core as pm from pymel import versions # mGear imports import mgear from mgear.vendor.Qt import QtCore from mgear.vendor.Qt import QtWidgets from mgear.core import pyqt from mgear.core import dag from mgear.core import transform from mgear.core import utils from mgear.core import attribute from mgear.core import vector from mgear.core.attribute import reset_selected_channels_value from mgear.core.pickWalk import get_all_tag_children # ============================================================================= # constants # ============================================================================= EXPR_LEFT_SIDE = re.compile("L(\d+)") EXPR_RIGHT_SIDE = re.compile("R(\d+)") CTRL_GRP_SUFFIX = "_controllers_grp" PLOT_GRP_SUFFIX = "_PLOT_grp" # spine FK/IK matching, naming ------------------------------------------------ TAN_TOKEN = "_tan_ctl" TAN0_TOKEN = "_tan0_ctl" TAN1_TOKEN = "_tan1_ctl" START_IK_TOKEN = "_ik0_ctl" END_IK_TOKEN = "_ik1_ctl" POS_IK_TOKEN = "_spinePosition_ctl" # No mirror attributes ------------------------------------------------ NO_MIRROR_ATTRIBUTES = ["isRig", "uiHost", "_ctl"] ################################################## # util def isSideElement(name): """Returns is name(str) side element? Arguments: name (str): Description Returns: bool Deleted Parameters: node: str """ if "_L_" in name or "_R_" in name: return True nameParts = stripNamespace(name).split("|")[-1] for part in nameParts.split("_"): if EXPR_LEFT_SIDE.match(part) or EXPR_RIGHT_SIDE.match(part): return True else: return False def swapSideLabel(name): """Returns fliped name Returns fliped name that replaced side label left to right or right to left Arguments: name(str): Name to swap the side Returns: str """ for part in name.split("_"): if EXPR_LEFT_SIDE.match(part): return EXPR_LEFT_SIDE.sub(r"R\1", name) if EXPR_RIGHT_SIDE.match(part): return EXPR_RIGHT_SIDE.sub(r"L\1", name) else: if "_L_" in name: return name.replace("_L_", "_R_") elif "_R_" in name: return name.replace("_R_", "_L_") else: return name def getClosestNode(node, nodesToQuery): """return the closest node, based on distance, from the list provided Args: node (string): name of node nodesToQuery (list): of nodes to query Returns: string: name of the closest node """ distance = None closestNode = None node = pm.PyNode(node) for index, nodeTQ in enumerate(nodesToQuery): nodeTQ = pm.PyNode(nodeTQ) tmpDist = vector.getDistance2(node, nodeTQ) if index is 0: distance = tmpDist closestNode = nodeTQ if distance > tmpDist: distance = tmpDist closestNode = nodeTQ return closestNode.name() def recordNodesMatrices(nodes, desiredTime): """get the matrices of the nodes provided and return a dict of node:matrix Args: nodes (list): of nodes Returns: dict: node:node matrix """ nodeToMat_dict = {} for fk in nodes: fk = pm.PyNode(fk) nodeToMat_dict[fk.name()] = fk.getAttr("worldMatrix", time=desiredTime) return nodeToMat_dict def getRootNode(): """Returns the root node from a selected node Returns: PyNode: The root top node """ root = None current = pm.ls(sl=True) if not current: raise RuntimeError("You need to select at least one rig node") if pm.objExists("{}.is_rig".format(current[0])): root = current[0] else: holder = current[0] while pm.listRelatives(holder, parent=True) and not root: if pm.objExists("{}.is_rig".format(holder)): root = holder else: holder = pm.listRelatives(holder, parent=True)[0] if not root: raise RuntimeError("Couldn't find root node from your selection") return root def getControlers(model, gSuffix=CTRL_GRP_SUFFIX): """Get thr controlers from the set Args: model (PyNode): Rig root gSuffix (str, optional): set suffix Returns: list: The members of the group """ try: ctl_set = pm.PyNode(model.name() + gSuffix) members = ctl_set.members() return members except TypeError: return None def get_ik_fk_controls(control, blend_attr): """ Returns the ik and fk controls related to the given control blend attr Args: control (str): uihost control to interact with blend_attr (str): attribute containing control list Returns: dict: fk and ik controls list on a dict """ ik_fk_controls = {"fk_controls": [], "ik_controls": []} controls_attribute = blend_attr.replace("_blend", "_ctl") try: controls = cmds.getAttr("{}.{}".format(control, controls_attribute)) except ValueError: if control == "world_ctl": _msg = "New type attributes using world as host are not supported" raise RuntimeError(_msg) attr = "{}_{}_ctl".format(blend_attr.split("_")[0], control.split(":")[-1].split("_")[1]) controls = cmds.getAttr("{}.{}".format(control, attr)) # filters the controls for ctl in controls.split(","): if len(ctl) == 0: continue ctl_type = ctl.split("_")[2] # filters ik controls if "ik" in ctl_type or "upv" in ctl_type: ik_fk_controls["ik_controls"].append(ctl) # filters fk controls elif "fk" in ctl_type: ik_fk_controls["fk_controls"].append(ctl) return ik_fk_controls def get_host_from_node(control): """Returns the host control name from the given control Args: control (str): Rig control Returns: str: Host UI control name """ # get host control namespace = getNamespace(control).split("|")[-1] host = cmds.getAttr("{}.uiHost".format(control)) return "{}:{}".format(namespace, host) def getNamespace(modelName): """Get the name space from rig root Args: modelName (str): Rig top node name Returns: str: Namespace """ if not modelName: return "" if len(modelName.split(":")) >= 2: nameSpace = ":".join(modelName.split(":")[:-1]) else: nameSpace = "" return nameSpace def stripNamespace(nodeName): """Strip all the namespaces from a given name Args: nodeName (str): Node name to strip the namespaces Returns: str: Node name without namespace """ return nodeName.split(":")[-1] def getNode(nodeName): """Get a PyNode from the string name Args: nodeName (str): Node name Returns: PyNode or None: The node. or None if the object can't be found """ try: return pm.PyNode(nodeName) except pm.MayaNodeError: return None def listAttrForMirror(node): """List attributes to invert the value for mirror posing Args: node (PyNode): The Node with the attributes to invert Returns: list: Attributes to invert """ # TODO: should "ro" be here? res = ["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz"] res.extend(pm.listAttr(node, userDefined=True, shortNames=True)) res = list(filter(lambda x: not x.startswith("inv"), res)) return res def getInvertCheckButtonAttrName(str): """Get the invert check butto attribute name Args: str (str): The attribute name Returns: str: The checked attribute name """ # type: (str) -> str return "inv{0}".format(str.lower().capitalize()) def selAll(model): """Select all controlers Args: model (PyNode): Rig top node """ controlers = getControlers(model) pm.select(controlers) def selGroup(model, groupSuffix): """Select the members of a given set Args: model (PyNode): Rig top node groupSuffix (str): Set suffix name """ controlers = getControlers(model, groupSuffix) pm.select(controlers) def select_all_child_controls(control, *args): # @unusedVariable """ Selects all child controls from the given control This function uses Maya's controller nodes and commands to find relevant dependencies between controls Args: control (str): parent animation control (transform node) *args: State of the menu item (if existing) send by mgear's dagmenu """ # gets controller node from the given control. Returns if none is found tag = cmds.ls(cmds.listConnections(control), type="controller") if not tag: return # query child controls children = get_all_tag_children(tag) # adds to current selection the children elements cmds.select(children, add=True) def quickSel(model, channel, mouse_button): """Select the object stored on the quick selection attributes Args: model (PyNode): The rig top node channel (str): The quick selection channel name mouse_button (QtSignal): Clicked mouse button Returns: None """ qs_attr = model.attr("quicksel%s" % channel) if mouse_button == QtCore.Qt.LeftButton: # Call Selection names = qs_attr.get().split(",") if not names: return pm.select(clear=True) for name in names: ctl = dag.findChild(model, name) if ctl: ctl.select(add=True) elif mouse_button == QtCore.Qt.MidButton: # Save Selection names = [sel.name().split("|")[-1] for sel in pm.ls(selection=True) if sel.name().endswith("_ctl")] qs_attr.set(",".join(names)) elif mouse_button == QtCore.Qt.RightButton: # Key Selection names = qs_attr.get().split(",") if not names: return else: keyObj(model, names) ################################################## # KEY ################################################## # ================================================ def keySel(): """Key selected controls""" pm.setKeyframe() # ================================================ def keyObj(model, object_names): """Set the keyframe in the controls pass by a list in obj_names variable Args: model (Str): Name of the namespace that will define de the model object_names (Str): names of the controls, without the name space Returns: None """ with pm.UndoChunk(): nodes = [] nameSpace = getNamespace(model) for name in object_names: if nameSpace: node = getNode(nameSpace + ":" + name) else: node = getNode(name) if not node: continue if not node and nameSpace: mgear.log("Can't find object : %s:%s" % (nameSpace, name), mgear.sev_error) elif not node: mgear.log("Can't find object : %s" % (name), mgear.sev_error) nodes.append(node) if not nodes: return pm.setKeyframe(*nodes) def keyAll(model): """Keyframe all the controls inside the controls group Note: We use the workd "group" to refer to a set in Maya Args: model (PyNode): Rig top node """ controlers = getControlers(model) pm.setKeyframe(controlers) def keyGroup(model, groupSuffix): """Keyframe all the members of a given group Args: model (PyNode): Rig top node groupSuffix (str): The group preffix """ controlers = getControlers(model, groupSuffix) pm.setKeyframe(controlers) # ================================================ def toggleAttr(model, object_name, attr_name): """Toggle a boolean attribute Args: model (PyNode): Rig top node object_name (str): The name of the control containing the attribute to toggle attr_name (str): The attribute to toggle """ nameSpace = getNamespace(model) if nameSpace: node = dag.findChild(nameSpace + ":" + object_name) else: node = dag.findChild(model, object_name) oAttr = node.attr(attr_name) if oAttr.type() in ["float", "bool"]: oVal = oAttr.get() if oVal == 1: oAttr.set(0) else: oAttr.set(1) # ================================================ def getComboIndex_with_namespace(namespace, object_name, combo_attr): """Get the index from a combo attribute Args: namespace (str): namespace object_name (str): Control name combo_attr (str): Combo attribute name Returns: int: Current index in the combo attribute """ if namespace: node = getNode(namespace + ":" + stripNamespace(object_name)) else: node = getNode(object_name) oVal = node.attr(combo_attr).get() return oVal def getComboIndex(model, object_name, combo_attr): """Get the index from a combo attribute Args: model (PyNode): Rig top node object_name (str): Control name combo_attr (str): Combo attribute name Returns: int: Current index in the combo attribute """ nameSpace = getNamespace(model) return getComboIndex_with_namespace(nameSpace, object_name, combo_attr) def changeSpace_with_namespace(namespace, object_name, combo_attr, cnsIndex, ctl_name): """Change the space of a control i.e: A control with ik reference array Args: namespace (str): namespace object_name (str): Object Name combo_attr (str): Combo attribute name cnsIndex (int): Combo index to change ctl_name (str): Control name """ if namespace: node = getNode(namespace + ":" + stripNamespace(object_name)) ctl = getNode(namespace + ":" + stripNamespace(ctl_name)) else: node = getNode(object_name) ctl = getNode(ctl_name) sWM = ctl.getMatrix(worldSpace=True) oAttr = node.attr(combo_attr) oAttr.set(cnsIndex) ctl.setMatrix(sWM, worldSpace=True) def changeSpace(model, object_name, combo_attr, cnsIndex, ctl_name): """Change the space of a control i.e: A control with ik reference array Args: model (PyNode): Rig top node object_name (str): Object Name combo_attr (str): Combo attribute name cnsIndex (int): Combo index to change ctl_name (str): Control name """ nameSpace = getNamespace(model) return changeSpace_with_namespace(nameSpace, object_name, combo_attr, cnsIndex, ctl_name) def change_rotate_order(control, target_order): """Change current control rotate order on all frames Args: control (str): control to interact on target_order (str): target rotate order """ if len(target_order) != 3: raise AttributeError("Your target rotate order is not valid. " "Please use any of the following: " "xyz, yzx, zxy, xzy, yxz, zyx") if not cmds.getAttr("{}.rotateOrder".format(control), settable=True): raise RuntimeError("RotateOrder is locked on the given control") # Maya's rotate order's index rotate_orders = {"xyz": 0, "yzx": 1, "zxy": 2, "xzy": 3, "yxz": 4, "zyx": 5 } # gets current control rotate order current_order = cmds.getAttr("{}.rotateOrder".format(control)) # do nothing if target rotate order is the same as current one if current_order == rotate_orders[target_order]: return # gets anim curves on rotation values anim_curves = [] for axe in ["x", "y", "z"]: anim_curves.extend(cmds.listConnections("{}.r{}".format(control, axe), type="animCurve") or []) # gets keyframe on rotateOrder attribute if any rotate_order_anim = cmds.listConnections("{}.rotateOrder".format(control), type="animCurve") or [] # get unique timeline values for all rotate keyframe frames = [] for node in anim_curves: [frames.append(x) for x in cmds.keyframe(node, query=True, controlPoints=True) if x not in frames] # pauses viewport update current_frame = cmds.currentTime(query=True) if not cmds.ogs(query=True, pause=True): cmds.ogs(pause=True) # stores matrix position of your control for each frame positions = {} holder = cmds.createNode("transform", name="{}_rotate_order_switch" .format(control.split("|")[-1])) cmds.setAttr("{}.rotateOrder".format(holder, rotate_orders[target_order])) for frame in frames: cmds.currentTime(frame) position = cmds.xform(control, query=True, worldSpace=True, matrix=True) cmds.xform(holder, worldSpace=True, matrix=position) positions[frame] = cmds.xform(holder, query=True, worldSpace=True, matrix=True) # change rotate order if rotate_order_anim: cmds.keyframe(rotate_order_anim, edit=True, valueChange=rotate_orders[target_order]) else: cmds.setAttr("{}.rotateOrder".format(control), rotate_orders[target_order]) for frame in frames: cmds.currentTime(frame) cmds.xform(control, worldSpace=True, matrix=positions[frame]) # filters curves cmds.filterCurve(anim_curves) # deletes holder and set back the good timeline value cmds.delete(holder) cmds.currentTime(current_frame) # un-pauses viewport if cmds.ogs(query=True, pause=True): cmds.ogs(pause=True) cmds.select(control) ################################################## # Combo Box ################################################## # ================================================ def getComboKeys_with_namespace(namespace, object_name, combo_attr): """Get the keys from a combo attribute Args: namespace (str): namespace object_name (str): Control name combo_attr (str): Combo attribute name Returns: list: Keys names from the combo attribute. """ if namespace: node = getNode(namespace + ":" + stripNamespace(object_name)) else: node = getNode(object_name) oAttr = node.attr(combo_attr) keys = oAttr.getEnums().keys() keys.append("++ Space Transfer ++") return keys def getComboKeys(model, object_name, combo_attr): """Get the keys from a combo attribute Args: model (PyNode): Rig top node object_name (str): Control name combo_attr (str): Combo attribute name Returns: list: Keys names from the combo attribute. """ nameSpace = getNamespace(model) return getComboKeys_with_namespace(nameSpace, object_name, combo_attr) ################################################## # IK FK switch match ################################################## # ================================================ def ikFkMatch_with_namespace(namespace, ikfk_attr, ui_host, fks, ik, upv, ik_rot=None, key=None): """Switch IK/FK with matching functionality This function is meant to work with 2 joint limbs. i.e: human legs or arms Args: namespace (str): Rig name space ikfk_attr (str): Blend ik fk attribute name ui_host (str): Ui host name fks ([str]): List of fk controls names ik (str): Ik control name upv (str): Up vector control name ikRot (None, str): optional. Name of the Ik Rotation control key (None, bool): optional. Whether we do an snap with animation """ # returns a pymel node on the given name def _get_node(name): # type: (str) -> pm.nodetypes.Transform name = stripNamespace(name) if namespace: node = getNode(":".join([namespace, name])) else: node = getNode(name) if not node: mgear.log("Can't find object : {0}".format(name), mgear.sev_error) return node # returns matching node def _get_mth(name): # type: (str) -> pm.nodetypes.Transform tmp = name.split("_") tmp[-1] = "mth" return _get_node("_".join(tmp)) # get things ready fk_ctrls = [_get_node(x) for x in fks] fk_targets = [_get_mth(x) for x in fks] ik_ctrl = _get_node(ik) ik_target = _get_mth(ik) upv_ctrl = _get_node(upv) if ik_rot: ik_rot_node = _get_node(ik_rot) ik_rot_target = _get_mth(ik_rot) ui_node = _get_node(ui_host) o_attr = ui_node.attr(ikfk_attr) val = o_attr.get() # sets keyframes before snapping if key: _all_controls = [] _all_controls.extend(fk_ctrls) _all_controls.extend([ik_ctrl, upv_ctrl, ui_node]) if ik_rot: _all_controls.extend([ik_rot_node]) [cmds.setKeyframe("{}".format(elem), time=(cmds.currentTime(query=True) - 1.0)) for elem in _all_controls] # if is IKw then snap FK if val == 1.0: for target, ctl in zip(fk_targets, fk_ctrls): transform.matchWorldTransform(target, ctl) o_attr.set(0.0) # if is FKw then sanp IK elif val == 0.0: transform.matchWorldTransform(ik_target, ik_ctrl) if ik_rot: transform.matchWorldTransform(ik_rot_target, ik_rot_node) transform.matchWorldTransform(fk_targets[1], upv_ctrl) # calculates new pole vector position start_end = (fk_targets[-1].getTranslation(space="world") - fk_targets[0].getTranslation(space="world")) start_mid = (fk_targets[1].getTranslation(space="world") - fk_targets[0].getTranslation(space="world")) dot_p = start_mid * start_end proj = float(dot_p) / float(start_end.length()) proj_vector = start_end.normal() * proj arrow_vector = start_mid - proj_vector arrow_vector *= start_end.normal().length() final_vector = (arrow_vector + fk_targets[1].getTranslation(space="world")) upv_ctrl.setTranslation(final_vector, space="world") # sets blend attribute new value o_attr.set(1.0) roll_att = ui_node.attr(ikfk_attr.replace("blend", "roll")) roll_att.set(0.0) # sets keyframes if key: [cmds.setKeyframe("{}".format(elem), time=(cmds.currentTime(query=True))) for elem in _all_controls] def ikFkMatch(model, ikfk_attr, ui_host, fks, ik, upv, ik_rot=None, key=None): """Switch IK/FK with matching functionality This function is meant to work with 2 joint limbs. i.e: human legs or arms Args: model (PyNode): Rig top transform node ikfk_attr (str): Blend ik fk attribute name ui_host (str): Ui host name fks ([str]): List of fk controls names ik (str): Ik control name upv (str): Up vector control name ikRot (None, str): optional. Name of the Ik Rotation control key (None, bool): optional. Whether we do an snap with animation """ # gets namespace current_namespace = getNamespace(model) ikFkMatch_with_namespace(current_namespace, ikfk_attr, ui_host, fks, ik, upv, ik_rot=ik_rot, key=key) # ============================================================================== # spine ik/fk matching/switching # ============================================================================== def spine_IKToFK(fkControls, ikControls, matchMatrix_dict=None): """position the IK controls to match, as best they can, the fk controls. Supports component: spine_S_shape_01, spine_ik_02 Args: fkControls (list): list of fk controls, IN THE ORDER OF HIERARCHY, ["spine_C0_fk0_ctl", ..., ..., "spine_C0_fk6_ctl"] ikControls (list): all ik controls """ if matchMatrix_dict is None: currentTime = pm.currentTime(q=True) matchMatrix_dict = recordNodesMatrices(fkControls, desiredTime=currentTime) attribute.reset_SRT(ikControls) for fk in fkControls: fk = pm.PyNode(fk) fk.setMatrix(matchMatrix_dict[fk.name()], worldSpace=True) def spine_FKToIK(fkControls, ikControls, matchMatrix_dict=None): """Match the IK controls to the FK. Known limitations: Does not compensate for stretching. Does not support zig-zag, or complex fk to ik transfers. Supports component: spine_S_shape_01, spine_ik_02 Args: fkControls (list): of of nodes, IN THE ORDER OF HIERARCHY ikControls (list): of of nodes """ # record the position of controls prior to reseting if matchMatrix_dict is None: currentTime = pm.currentTime(q=True) matchMatrix_dict = recordNodesMatrices(fkControls, desiredTime=currentTime) # reset both fk, ik controls attribute.reset_SRT(ikControls) attribute.reset_SRT(fkControls) rootFk = fkControls[0] endFk = fkControls[-1] # get the ik controls sorted from the list provided tan1Ctl = [pm.PyNode(ik) for ik in ikControls if TAN1_TOKEN in ik][0] tan0Ctl = [pm.PyNode(ik) for ik in ikControls if TAN0_TOKEN in ik][0] # get the ik controls sorted from the list provided ik1Ctl = [pm.PyNode(ik) for ik in ikControls if END_IK_TOKEN in ik][0] ik0Ctl = [pm.PyNode(ik) for ik in ikControls if START_IK_TOKEN in ik][0] # optional controls ikPosCtl = [pm.PyNode(ik) for ik in ikControls if POS_IK_TOKEN in ik] tanCtl = [pm.PyNode(ik) for ik in ikControls if TAN_TOKEN in ik] # while the nodes are reset, get the closest counterparts if tanCtl: closestFk2Tan = getClosestNode(tanCtl[0], fkControls) closestFk2Tan1 = getClosestNode(tan1Ctl, fkControls) closestFk2Tan0 = getClosestNode(tan0Ctl, fkControls) # optional controls if they exist if ikPosCtl: ikPosCtl[0].setMatrix(matchMatrix_dict[endFk], worldSpace=True) # constrain the top and bottom of the ik controls ik0Ctl.setMatrix(matchMatrix_dict[rootFk], worldSpace=True) ik1Ctl.setMatrix(matchMatrix_dict[endFk], worldSpace=True) if tanCtl: tanCtl[0].setMatrix(matchMatrix_dict[closestFk2Tan], worldSpace=True) # contrain the tan controls tan0Ctl.setMatrix(matchMatrix_dict[closestFk2Tan0], worldSpace=True) tan1Ctl.setMatrix(matchMatrix_dict[closestFk2Tan1], worldSpace=True) ################################################## # POSE ################################################## def mirrorPose(flip=False, nodes=None): """Summary Args: flip (bool, optiona): Set the function behaviout to flip nodes (None, [PyNode]): Controls to mirro/flip the pose """ if nodes is None: nodes = pm.selected() pm.undoInfo(ock=1) try: nameSpace = False if nodes: nameSpace = getNamespace(nodes[0]) mirrorEntries = [] for oSel in nodes: mirrorEntries.extend(gatherMirrorData(nameSpace, oSel, flip)) for dat in mirrorEntries: applyMirror(nameSpace, dat) except Exception as e: pm.displayWarning("Flip/Mirror pose fail") import traceback traceback.print_exc() print e finally: pm.undoInfo(cck=1) def applyMirror(nameSpace, mirrorEntry): """Apply mirro pose Args: nameSpace (str): Namespace mirrorEntry (list): List with the mirror entry template """ node = mirrorEntry["target"] attr = mirrorEntry["attr"] val = mirrorEntry["val"] for skip in NO_MIRROR_ATTRIBUTES: if attr.count(skip): return try: if (pm.attributeQuery(attr, node=node, shortName=True, exists=True) and not node.attr(attr).isLocked()): node.attr(attr).set(val) except RuntimeError as e: mgear.log("applyMirror failed: {0} {1}: {2}" .format(node.name(), attr, e), mgear.sev_error) def gatherMirrorData(nameSpace, node, flip): """Get the data to mirror Args: nameSpace (str): Namespace node (PyNode): No flip (TYPE): flip option Returns: [dict[str]: The mirror data """ if isSideElement(node.name()): nameParts = stripNamespace(node.name()).split("|")[-1] nameParts = swapSideLabel(nameParts) nameTarget = ":".join([nameSpace, nameParts]) oTarget = getNode(nameTarget) return calculateMirrorData(node, oTarget, flip=flip) else: return calculateMirrorData(node, node, flip=False) def calculateMirrorData(srcNode, targetNode, flip=False): """Calculate the mirror data Args: srcNode (str): The source Node targetNode ([dict[str]]): Target node flip (bool, optional): flip option Returns: [{"target": node, "attr": at, "val": flipVal}] """ results = [] # mirror attribute of source for attrName in listAttrForMirror(srcNode): # whether does attribute "invTx" exists when attrName is "tx" invCheckName = getInvertCheckButtonAttrName(attrName) if not pm.attributeQuery(invCheckName, node=srcNode, shortName=True, exists=True): # if not exists, straight inv = 1 else: # if exists, check its value invAttr = srcNode.attr(invCheckName) if invAttr.get(): inv = -1 else: inv = 1 # if attr name is side specified, record inverted attr name if isSideElement(attrName): invAttrName = swapSideLabel(attrName) else: invAttrName = attrName # if flip enabled record self also if flip: flipVal = targetNode.attr(attrName).get() results.append({"target": srcNode, "attr": invAttrName, "val": flipVal * inv}) results.append({"target": targetNode, "attr": invAttrName, "val": srcNode.attr(attrName).get() * inv}) return results def mirrorPoseOld(flip=False, nodes=False): """Deprecated: Mirror pose Args: flip (bool, optional): if True will flip the pose nodes (bool, optional): Nodes to mirror or flip transformation """ axis = ["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz"] aDic = {"tx": "invTx", "ty": "invTy", "tz": "invTz", "rx": "invRx", "ry": "invRy", "rz": "invRz", "sx": "invSx", "sy": "invSy", "sz": "invSz"} mapDic = {"L": "R", "R": "L"} if not nodes: nodes = pm.selected() pm.undoInfo(ock=1) try: nameSpace = False if nodes: if len(nodes[0].split(":")) == 2: nameSpace = nodes[0].split(":")[0] for oSel in nodes: if nameSpace: nameParts = oSel.name().split(":")[1].split("|")[-1].split("_") else: nameParts = oSel.name().split("|")[-1].split("_") if nameParts[1][0] == "C": if not oSel.attr("tx").isLocked(): oSel.attr("tx").set(oSel.attr("tx").get() * -1) if not oSel.attr("ry").isLocked(): oSel.attr("ry").set(oSel.attr("ry").get() * -1) if not oSel.attr("rz").isLocked(): oSel.attr("rz").set(oSel.attr("rz").get() * -1) else: nameParts[1] = mapDic[nameParts[1][0]] + nameParts[1][1:] if nameSpace: nameTarget = nameSpace + ":" + "_".join(nameParts) else: nameTarget = "_".join(nameParts) oTarget = getNode(nameTarget) for a in axis: if not oSel.attr(a).isLocked(): if oSel.attr(aDic[a]).get(): inv = -1 else: inv = 1 if flip: flipVal = oTarget.attr(a).get() oTarget.attr(a).set(oSel.attr(a).get() * inv) if flip: oSel.attr(a).set(flipVal * inv) except Exception: pm.displayWarning("Flip/Mirror pose fail") pass finally: pm.undoInfo(cck=1) def bindPose(model): """Restore the reset position of the rig Args: model (TYPE): Description """ nameSpace = getNamespace(model.name()) if nameSpace: dagPoseName = nameSpace + ':dagPose1' else: dagPoseName = 'dagPose1' pm.dagPose(dagPoseName, restore=True) def resetSelTrans(): """Reset the transfom values (SRT) for the selected objects""" with pm.UndoChunk(): for obj in pm.selected(): transform.resetTransform(obj) def reset_all_keyable_attributes(dagnodes, *args): # @unusedVariable """Resets to default values all keyable attributes on the given node Args: dagnodes (list): Maya transform nodes to reset *args: State of the menu item (if existing) send by mgear's dagmenu """ for node in dagnodes: keyable_attrs = cmds.listAttr(node, keyable=True) reset_selected_channels_value([node], keyable_attrs) ################################################## # Transfer space ################################################## class AbstractAnimationTransfer(QtWidgets.QDialog): """Abstract animation transfer class""" try: valueChanged = QtCore.Signal(int) except Exception: valueChanged = pyqt.pyqtSignal() def __init__(self): # type: () -> None self.comboObj = None # type: widgets.toggleCombo self.comboItems = [] # type: list[str] self.model = None # type: pm.nodetypes.Transform self.uihost = None # type: str self.switchedAttrShortName = None # type: str def createUI(self, parent=None): # type: (QtWidgets.QObject) -> None super(AbstractAnimationTransfer, self).__init__(parent) self.setWindowTitle("Space Transfer") self.setWindowFlags(QtCore.Qt.Tool) self.setAttribute(QtCore.Qt.WA_DeleteOnClose, 1) self.create_controls() self.create_layout() self.create_connections() def create_controls(self): # type: () -> None self.groupBox = QtWidgets.QGroupBox() # must be implemented in each specialized classes self.setGroupBoxTitle() self.onlyKeyframes_check = QtWidgets.QCheckBox('Only Keyframe Frames') self.onlyKeyframes_check.setChecked(True) self.startFrame_label = QtWidgets.QLabel("Start") self.startFrame_value = QtWidgets.QSpinBox() self.startFrame_value = QtWidgets.QSpinBox() self.startFrame_value.setMinimum(-999999) self.startFrame_value.setMaximum(999999) self.endFrame_label = QtWidgets.QLabel("End") self.endFrame_value = QtWidgets.QSpinBox() self.endFrame_value.setMinimum(-999999) self.endFrame_value.setMaximum(999999) self.populateRange(True) self.allFrames_button = QtWidgets.QPushButton("All Frames") self.timeSliderFrames_button = QtWidgets.QPushButton( "Time Slider Frames") self.comboBoxSpaces = QtWidgets.QComboBox() self.comboBoxSpaces.addItems(self.comboItems) if self.comboObj is not None: # this add suport QlistWidget if isinstance(self.comboObj, QtWidgets.QListWidget): idx = self.comboObj.currentRow() else: idx = self.comboObj.currentIndex() self.comboBoxSpaces.setCurrentIndex(idx) self.spaceTransfer_button = QtWidgets.QPushButton("Space Transfer") def create_layout(self): # type: () -> None frames_layout = QtWidgets.QHBoxLayout() frames_layout.setContentsMargins(1, 1, 1, 1) frames_layout.addWidget(self.startFrame_label) frames_layout.addWidget(self.startFrame_value) frames_layout.addWidget(self.endFrame_label) frames_layout.addWidget(self.endFrame_value) framesSetter_layout = QtWidgets.QHBoxLayout() framesSetter_layout.setContentsMargins(1, 1, 1, 1) framesSetter_layout.addWidget(self.allFrames_button) framesSetter_layout.addWidget(self.timeSliderFrames_button) paremeter_layout = QtWidgets.QVBoxLayout(self.groupBox) paremeter_layout.setContentsMargins(6, 5, 6, 5) paremeter_layout.addWidget(self.onlyKeyframes_check) paremeter_layout.addLayout(frames_layout) paremeter_layout.addLayout(framesSetter_layout) paremeter_layout.addWidget(self.comboBoxSpaces) paremeter_layout.addWidget(self.spaceTransfer_button) spaceTransfer_layout = QtWidgets.QVBoxLayout() spaceTransfer_layout.addWidget(self.groupBox) self.setLayout(spaceTransfer_layout) def create_connections(self): # type: () -> None self.spaceTransfer_button.clicked.connect(self.doItByUI) self.allFrames_button.clicked.connect( partial(self.populateRange, False)) self.timeSliderFrames_button.clicked.connect( partial(self.populateRange, True)) # SLOTS ########################################################## def populateRange(self, timeSlider=False): # type: (bool) -> None if timeSlider: start = pm.playbackOptions(q=True, min=True) end = pm.playbackOptions(q=True, max=True) else: start = pm.playbackOptions(q=True, ast=True) end = pm.playbackOptions(q=True, aet=True) self.startFrame_value.setValue(start) self.endFrame_value.setValue(end) def setComboBoxItemsFormComboObj(self, combo): # type: (widegts.toggleCombo or QtWidgets.QListWidget) -> None del self.comboItems[:] for i in range(combo.count() - 1): # this add suport QlistWidget if isinstance(combo, QtWidgets.QListWidget): self.comboItems.append(combo.item(i).text()) else: self.comboItems.append(combo.itemText(i)) def setComboBoxItemsFormList(self, comboList): # type: (list[str]) -> None del self.comboItems[:] for i in range(len(comboList)): self.comboItems.append(comboList[i]) # ---------------------------------------------------------------- def setGroupBoxTitle(self): # type: (str) -> None # raise NotImplementedError("must implement transfer # in each specialized class") pass def setComboObj(self, combo): # type: (widgets.toggleCombo) -> None self.comboObj = combo def setModel(self, model): # type: (pm.nodetypes.Transform) -> None self.model = model self.nameSpace = getNamespace(self.model) def setUiHost(self, uihost): # type: (str) -> None self.uihost = uihost def setSwitchedAttrShortName(self, attr): # type: (str) -> None self.switchedAttrShortName = attr def getHostName(self): # type: () -> str return ":".join([self.nameSpace, self.uihost]) def getWorldMatrices(self, start, end, val_src_nodes): # type: (int, int, List[pm.nodetypes.Transform]) -> # List[List[pm.datatypes.Matrix]] """ returns matrice List[frame][controller number].""" res = [] for x in range(start, end + 1): tmp = [] for n in val_src_nodes: tmp.append(pm.getAttr(n + '.worldMatrix', time=x)) res.append(tmp) return res def transfer(self, startFrame, endFrame, onlyKeyframes, *args, **kwargs): # type: (int, int, bool, *str, **str) -> None raise NotImplementedError("must be implemented in each " "specialized class") def doItByUI(self): # type: () -> None # gather settings from UI startFrame = self.startFrame_value.value() endFrame = self.endFrame_value.value() onlyKeyframes = self.onlyKeyframes_check.isChecked() # main body self.transfer(startFrame, endFrame, onlyKeyframes) # set the new space value in the synoptic combobox if self.comboObj is not None: if isinstance(self.comboObj, QtWidgets.QComboBox): self.comboObj.setCurrentIndex( self.comboBoxSpaces.currentIndex()) for c in pyqt.maya_main_window().children(): if isinstance(c, AbstractAnimationTransfer): c.deleteLater() @utils.one_undo @utils.viewport_off def bakeAnimation(self, switch_attr_name, val_src_nodes, key_src_nodes, key_dst_nodes, startFrame, endFrame, onlyKeyframes=True): # type: (str, List[pm.nodetypes.Transform], # List[pm.nodetypes.Transform], # List[pm.nodetypes.Transform], int, int, bool) -> None # Temporaly turn off cycle check to avoid misleading cycle message # on Maya 2016. With Maya 2016.5 and 2017 the cycle warning doesn't # show up # if versions.current() <= 20180200: pm.cycleCheck(e=False) pm.displayWarning("Maya version older than: 2016.5: " "CycleCheck temporal turn OFF") channels = ["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz"] worldMatrixList = self.getWorldMatrices(startFrame, endFrame, val_src_nodes) keyframeList = sorted(set(pm.keyframe(key_src_nodes, at=["t", "r", "s"], q=True))) # delete animation in the space switch channel and destination ctrls pm.cutKey(key_dst_nodes, at=channels, time=(startFrame, endFrame)) pm.cutKey(switch_attr_name, time=(startFrame, endFrame)) for i, x in enumerate(range(startFrame, endFrame + 1)): if onlyKeyframes and x not in keyframeList: continue pm.currentTime(x) # set the new space in the channel self.changeAttrToBoundValue() # bake the stored transforms to the cotrols for j, n in enumerate(key_dst_nodes): n.setMatrix(worldMatrixList[i][j], worldSpace=True) pm.setKeyframe(key_dst_nodes, at=channels) pm.setKeyframe(switch_attr_name) # if versions.current() <= 20180200: pm.cycleCheck(e=True) pm.displayWarning("CycleCheck turned back ON") # ================================================ # Transfer space class ParentSpaceTransfer(AbstractAnimationTransfer): def __init__(self): # type: () -> None super(ParentSpaceTransfer, self).__init__() # ---------------------------------------------------------------- def setCtrls(self, srcName): # type: (str) -> None self.ctrlNode = getNode(":".join([self.nameSpace, srcName])) def getChangeAttrName(self): # type: () -> str return "{}.{}".format(self.getHostName(), self.switchedAttrShortName) def changeAttrToBoundValue(self): # type: () -> None pm.setAttr(self.getChangeAttrName(), self.getValue()) def getValue(self): # type: () -> int return self.comboBoxSpaces.currentIndex() def setGroupBoxTitle(self): if hasattr(self, "groupBox"): # TODO: extract logic with naming convention part = "_".join( self.ctrlNode.name().split(":")[-1].split("_")[:-1]) self.groupBox.setTitle(part) def transfer(self, startFrame, endFrame, onlyKeyframes, *args, **kwargs): # type: (int, int, bool, *str, **str) -> None val_src_nodes = [self.ctrlNode] key_src_nodes = val_src_nodes key_dst_nodes = val_src_nodes self.bakeAnimation(self.getChangeAttrName(), val_src_nodes, key_src_nodes, key_dst_nodes, startFrame, endFrame, onlyKeyframes) @staticmethod def showUI(combo, model, uihost, switchedAttrShortName, ctrl_name, *args): # type: (widgets.toggleCombo, # pm.nodetypes.Transform, str, str, str, *str) -> None try: for c in pyqt.maya_main_window().children(): if isinstance(c, ParentSpaceTransfer): c.deleteLater() except RuntimeError: pass # Create minimal UI object ui = ParentSpaceTransfer() ui.setComboObj(combo) ui.setModel(model) ui.setUiHost(uihost) ui.setSwitchedAttrShortName(switchedAttrShortName) ui.setCtrls(ctrl_name) ui.setComboBoxItemsFormComboObj(ui.comboObj) # Delete the UI if errors occur to avoid causing winEvent # and event errors (in Maya 2014) try: ui.createUI(pyqt.maya_main_window()) ui.show() except Exception as e: ui.deleteLater() traceback.print_exc() mgear.log(e, mgear.sev_error) class IkFkTransfer(AbstractAnimationTransfer): def __init__(self): # type: () -> None super(IkFkTransfer, self).__init__() self.getValue = self.getValueFromUI # ---------------------------------------------------------------- def getChangeAttrName(self): # type: () -> str return "{}.{}".format(self.getHostName(), self.switchedAttrShortName) def getChangeRollAttrName(self): # type: () -> str return "{}.{}".format( self.getHostName(), self.switchedAttrShortName.replace("blend", "roll")) def changeAttrToBoundValue(self): # type: () -> None pm.setAttr(self.getChangeAttrName(), self.getValue()) def getValueFromUI(self): # type: () -> float if self.comboBoxSpaces.currentIndex() == 0: # IK return 1.0 else: # FK return 0.0 def _getNode(self, name): # type: (str) -> pm.nodetypes.Transform node = getNode(":".join([self.nameSpace, name])) if not node: mgear.log("Can't find object : {0}".format(name), mgear.sev_error) return node def _getMth(self, name): # type: (str) -> pm.nodetypes.Transform tmp = name.split("_") tmp[-1] = "mth" return self._getNode("_".join(tmp)) def setCtrls(self, fks, ik, upv, ikRot): # type: (list[str], str, str) -> None """gather core PyNode represented each controllers""" self.fkCtrls = [self._getNode(x) for x in fks] self.fkTargets = [self._getMth(x) for x in fks] self.ikCtrl = self._getNode(ik) self.ikTarget = self._getMth(ik) self.upvCtrl = self._getNode(upv) self.upvTarget = self._getMth(upv) if ikRot: self.ikRotCtl = self._getNode(ikRot) self.ikRotTarget = self._getMth(ikRot) self.hasIkRot = True else: self.hasIkRot = False def setGroupBoxTitle(self): if hasattr(self, "groupBox"): # TODO: extract logic with naming convention part = "_".join(self.ikCtrl.name().split(":")[-1].split("_")[:-2]) self.groupBox.setTitle(part) # ---------------------------------------------------------------- def transfer(self, startFrame, endFrame, onlyKeyframes, ikRot, switchTo=None, *args, **kargs): # type: (int, int, bool, str, *str, **str) -> None if switchTo is not None: if "fk" in switchTo.lower(): val_src_nodes = self.fkTargets key_src_nodes = [self.ikCtrl, self.upvCtrl] key_dst_nodes = self.fkCtrls if ikRot: key_src_nodes.append(self.ikRotCtl) else: val_src_nodes = [self.ikTarget, self.upvTarget] key_src_nodes = self.fkCtrls key_dst_nodes = [self.ikCtrl, self.upvCtrl] if ikRot: val_src_nodes.append(self.ikRotTarget) key_dst_nodes.append(self.ikRotCtl) # reset roll channel: roll_att = self.getChangeRollAttrName() pm.cutKey(roll_att, time=(startFrame, endFrame), cl=True) pm.setAttr(roll_att, 0) else: if self.comboBoxSpaces.currentIndex() != 0: # to FK val_src_nodes = self.fkTargets key_src_nodes = [self.ikCtrl, self.upvCtrl] key_dst_nodes = self.fkCtrls if ikRot: key_src_nodes.append(self.ikRotCtl) else: # to IK val_src_nodes = [self.ikTarget, self.upvTarget] key_src_nodes = self.fkCtrls key_dst_nodes = [self.ikCtrl, self.upvCtrl] if ikRot: val_src_nodes.append(self.ikRotTarget) key_dst_nodes.append(self.ikRotCtl) # reset roll channel: roll_att = self.getChangeRollAttrName() pm.cutKey(roll_att, time=(startFrame, endFrame)) pm.setAttr(roll_att, 0) self.bakeAnimation(self.getChangeAttrName(), val_src_nodes, key_src_nodes, key_dst_nodes, startFrame, endFrame, onlyKeyframes) # ---------------------------------------------------------------- # re implement doItbyUI to have access to self.hasIKrot option def doItByUI(self): # type: () -> None # gather settings from UI startFrame = self.startFrame_value.value() endFrame = self.endFrame_value.value() onlyKeyframes = self.onlyKeyframes_check.isChecked() # main body self.transfer(startFrame, endFrame, onlyKeyframes, self.hasIkRot) # set the new space value in the synoptic combobox if self.comboObj is not None: self.comboObj.setCurrentIndex(self.comboBoxSpaces.currentIndex()) for c in pyqt.maya_main_window().children(): if isinstance(c, AbstractAnimationTransfer): c.deleteLater() # ---------------------------------------------------------------- @staticmethod def showUI(model, ikfk_attr, uihost, fks, ik, upv, ikRot, *args): # type: (pm.nodetypes.Transform, str, str, # List[str], str, str, *str) -> None try: for c in pyqt.maya_main_window().children(): if isinstance(c, IkFkTransfer): c.deleteLater() except RuntimeError: pass # Create minimal UI object ui = IkFkTransfer() ui.setModel(model) ui.setUiHost(uihost) ui.setSwitchedAttrShortName(ikfk_attr) ui.setCtrls(fks, ik, upv, ikRot) ui.setComboObj(None) ui.setComboBoxItemsFormList(["IK", "FK"]) # Delete the UI if errors occur to avoid causing winEvent # and event errors (in Maya 2014) try: ui.createUI(pyqt.maya_main_window()) ui.show() except Exception as e: ui.deleteLater() traceback.print_exc() mgear.log(e, mgear.sev_error) @staticmethod def execute(model, ikfk_attr, uihost, fks, ik, upv, ikRot=None, startFrame=None, endFrame=None, onlyKeyframes=None, switchTo=None): # type: (pm.nodetypes.Transform, str, str, # List[str], str, str, int, int, bool, str) -> None """transfer without displaying UI""" if startFrame is None: startFrame = int(pm.playbackOptions(q=True, ast=True)) if endFrame is None: endFrame = int(pm.playbackOptions(q=True, aet=True)) if onlyKeyframes is None: onlyKeyframes = True if switchTo is None: switchTo = "fk" # Create minimal UI object ui = IkFkTransfer() ui.setComboObj(None) ui.setModel(model) ui.setUiHost(uihost) ui.setSwitchedAttrShortName(ikfk_attr) ui.setCtrls(fks, ik, upv, ikRot) ui.setComboBoxItemsFormList(["IK", "FK"]) ui.getValue = lambda: 0.0 if "fk" in switchTo.lower() else 1.0 ui.transfer(startFrame, endFrame, onlyKeyframes, ikRot, switchTo="fk") @staticmethod def toIK(model, ikfk_attr, uihost, fks, ik, upv, ikRot, **kwargs): # type: (pm.nodetypes.Transform, str, str, # List[str], str, str, **str) -> None kwargs.update({"switchTo": "ik"}) IkFkTransfer.execute(model, ikfk_attr, uihost, fks, ik, upv, ikRot, **kwargs) @staticmethod def toFK(model, ikfk_attr, uihost, fks, ik, upv, ikRot, **kwargs): # type: (pm.nodetypes.Transform, str, str, # List[str], str, str, **str) -> None kwargs.update({"switchTo": "fk"}) IkFkTransfer.execute(model, ikfk_attr, uihost, fks, ik, upv, ikRot, **kwargs) # Baker Springs @utils.one_undo def clearSprings(model=None): """Delete baked animation from spring Args: model (dagNode): The rig top node """ # filters the root node from selection if not model: model = getRootNode() springNodes = getControlers(model, gSuffix=PLOT_GRP_SUFFIX) pairblends = [sn.listConnections(type="pairBlend")[0] for sn in springNodes] for pb in pairblends: animCrvs = pb.listConnections(type="animCurveTA") for fcrv in animCrvs: for conn in fcrv.listConnections(connections=True, destination=True, plugs=True): pm.disconnectAttr(conn[0], conn[1]) # reset the value to 0 attrs = ["inRotateX1", "inRotateY1", "inRotateZ1"] for attr in attrs: pb.attr(attr).set(0) # delete fcurves pm.delete(animCrvs) @utils.one_undo @utils.viewport_off def bakeSprings(model=None): """Bake the automatic spring animation to animation curves Args: model (dagNode): The rig top node """ # filters the root node from selection if not model: model = getRootNode() print("Using root: {}".format(model)) # first clear animation clearSprings(model) # bake again springNodes = getControlers(model, gSuffix=PLOT_GRP_SUFFIX) if springNodes: start = pm.playbackOptions(q=True, min=True) end = pm.playbackOptions(q=True, max=True) ct = start for i in range(int(end - start) + 1): pm.currentTime(int(ct)) pm.setKeyframe(springNodes, insertBlend=True, attribute='rotate') ct += 1 class SpineIkFkTransfer(AbstractAnimationTransfer): def __init__(self): super(SpineIkFkTransfer, self).__init__() self.fkControls = None self.ikControls = None def doItByUI(self): """Gather UI settings to execute transfer """ startFrame = self.startFrame_value.value() endFrame = self.endFrame_value.value() onlyKeyframes = self.onlyKeyframes_check.isChecked() # based on user input, decide where to flatten animation bakeToIk = self.comboBoxSpaces.currentIndex() self.bakeAnimation(self.fkControls, self.ikControls, startFrame, endFrame, bakeToIk=bakeToIk, onlyKeyframes=onlyKeyframes) # Refresh the viewport by toggling time, refresh/dgdirty do not work pm.currentTime(startFrame) pm.currentTime(endFrame) # set the new space value in the synoptic combobox if self.comboObj is not None: self.comboObj.setCurrentIndex(self.comboBoxSpaces.currentIndex()) for c in pyqt.maya_main_window().children(): if isinstance(c, AbstractAnimationTransfer): c.deleteLater() def setCtrls(self, fkControls, ikControls): """make provided controls accessible to the class, with namespaces Args: fkControls (list): of fk controls ikControls (list): of ik controls """ ns = self.nameSpace if not ns.endswith(':') and ns != "": ns = "{0}:".format(ns) self.fkControls = ["{0}{1}".format(ns, x) for x in fkControls] self.ikControls = ["{0}{1}".format(ns, x) for x in ikControls] @staticmethod def showUI(topNode, uihost, fkControls, ikControls, *args): """Called from the synaptic qpushbutton, with the spine control names Args: topNode (string): top node of the rig uihost (TYPE): Description fkControls (list): of fkControls ikControls (list): of ikControls *args: additional signal args, n/a """ try: for c in pyqt.maya_main_window().children(): if isinstance(c, IkFkTransfer): c.deleteLater() except RuntimeError: pass # Create minimal UI object ui = SpineIkFkTransfer() ui.setModel(topNode) ui.setUiHost(uihost) ui.setCtrls(fkControls, ikControls) ui.setComboObj(None) ui.setComboBoxItemsFormList(["IK >> FK", "FK >> IK"]) # Delete the UI if errors occur to avoid causing winEvent # and event errors (in Maya 2014) try: ui.createUI(pyqt.maya_main_window()) ui.setWindowTitle('Spine IKFK') ui.show() except Exception as e: ui.deleteLater() traceback.print_exc() mgear.log(e, mgear.sev_error) @utils.one_undo @utils.viewport_off def bakeAnimation(self, fkControls, ikControls, startFrame, endFrame, bakeToIk=True, onlyKeyframes=True): """bake animation to desired destination. More adding animtion than ik/fk transfer Args: fkControls (list): of fk controls ikControls (list): of ik controls startFrame (float): start frame endFrame (float): end frame bakeToIk (bool, optional): True, bake animation to ik, fk is false onlyKeyframes (bool, optional): transfer animation on other keyframes, if false, bake every frame """ # Temporaly turn off cycle check to avoid misleading cycle message # on Maya 2016. With Maya 2016.5 and 2017 the cycle warning doesn't # show up if bakeToIk: key_src_nodes = fkControls transferFunc = spine_FKToIK key_dst_nodes = ikControls else: key_src_nodes = ikControls transferFunc = spine_IKToFK key_dst_nodes = fkControls # add all nodes, to get all of the keyframes allAnimNodes = fkControls + ikControls # remove duplicates keyframeList = sorted(set(pm.keyframe(allAnimNodes, at=["t", "r", "s"], q=True))) # when getAttr over time, it warns of a cycle if versions.current() <= 20180200: pm.cycleCheck(e=False) print "Maya version older than: 2018.02" # create a dict of every frame, and every node involved on that frame matchMatrix_dict = {} for i, x in enumerate(range(startFrame, endFrame + 1)): if onlyKeyframes and x not in keyframeList: continue matchMatrix_dict[x] = recordNodesMatrices(fkControls, x) channels = ["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz"] # delete animation in the channel and destination ctrls pm.cutKey(fkControls, at=channels, time=(startFrame, endFrame)) pm.cutKey(ikControls, at=channels, time=(startFrame, endFrame)) for frame, matchDict in matchMatrix_dict.iteritems(): pm.currentTime(frame) transferFunc(fkControls, ikControls, matchMatrix_dict=matchDict) pm.setKeyframe(key_dst_nodes, at=channels) # If there are keys on the source node outside of the provided range # this wont have an effect attribute.reset_SRT(key_src_nodes) # re enable cycle check if versions.current() <= 20180200: pm.cycleCheck(e=True) print "CycleCheck turned back ON"