diff --git a/release/scripts/mgear/core/anim_utils.py b/release/scripts/mgear/core/anim_utils.py index a655c91f..76768d6b 100644 --- a/release/scripts/mgear/core/anim_utils.py +++ b/release/scripts/mgear/core/anim_utils.py @@ -47,115 +47,130 @@ # util -def isSideElement(name): - """Returns is name(str) side element? - - Arguments: - name (str): Description +def getCtlRulePattern(guideData=None): + """Generates a regular expression pattern based on the control naming conventions + defined in the rig's guide data. The pattern can be used to validate and extract parts of a control's name. Returns: - bool + - str: A regex pattern corresponding to the rig's control naming convention. - Deleted Parameters: - node: str + Example: + Assuming the rig's naming rule is "{component}_{side}{index}_{extension}" + with "L", "R", and "C" as possible side values, and "_ctl" as the control extension, + the generated pattern might look something like: + "(?P.*?)_(?PL|R|C)(?P\d*)_(?P_ctl)" """ + if not guideData: + guideData = attribute.get_guide_data_from_rig() + guideInfo = guideData["guide_root"]["param_values"] - if "_L_" in name or "_R_" in name: - return True + # Extract relevant data + nameRule = guideInfo["ctl_name_rule"] + sides = (guideInfo["side_left_name"], + guideInfo["side_right_name"], + guideInfo["side_center_name"]) + ctlExt = guideInfo["ctl_name_ext"] - nameParts = stripNamespace(name).split("|")[-1] + patternDict = dict(component="(?P.*?)", + side="(?P{})".format("|".join(sides)), + index="(?P\d*)", + description="?(?P.*?)?", + extension="(?P{})".format(ctlExt)) - for part in nameParts.split("_"): - if EXPR_LEFT_SIDE.match(part) or EXPR_RIGHT_SIDE.match(part): - return True - else: - return False + return nameRule.format(**patternDict) -def isSideNode(node): - """Returns is name(str) side element? +def getSideLabelFromCtl(ctl): + """Extracts and returns the side label from a given control name based on the rig's naming convention. - Arguments: - name (node): PyNode + Parameters: + - ctl (str): The name of the control from which the side label should be extracted. Returns: - bool - """ + - str: The extracted side label (e.g., "L", "R", etc.). Returns "None" if no side label is found. - if node.hasAttr("side_label"): - if node.side_label.get() in "LR": - return True - else: - return False - - else: - return isSideElement(node.name()) + Example: + > getSideLabelFromCtl("arm_L_01_ctl") + "L" + """ + matchResult = re.match(getCtlRulePattern(), ctl) + side = matchResult.group("side") if matchResult else "None" + return side -def swapSideLabel(name): - """Returns fliped name +def swapSideLabel(name, guideData=None): + """Swap the side label of a given control name, based on the rig's naming convention. - Returns fliped name that replaced side label left to right or - right to left + The function uses the guide data's naming patterns to identify the current side label + (e.g., "L", "R", etc.) in the provided name, and swaps it to the opposite side. + If the side is "center" or not recognized, the original name is returned. - Arguments: - name(str): Name to swap the side + Parameters: + - name (str): The name of the control. Returns: - str + - str: The name with the swapped side label. If the side label can't be swapped, + the original name is returned. + + Example: + swapSideLabel("arm_L_01_ctl") + "arm_R_01_ctl" """ + if not guideData: + guideData = attribute.get_guide_data_from_rig() + guideInfo = guideData["guide_root"]["param_values"] + + sideInfo = { + "left": guideInfo["side_left_name"], + "right": guideInfo["side_right_name"], + "center": guideInfo["side_center_name"] + } - 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) + ctlPattern = re.compile(getCtlRulePattern()) + matchResult = ctlPattern.search(name) + if not matchResult: + return - else: - if "_L_" in name: - return name.replace("_L_", "_R_") - elif "_R_" in name: - return name.replace("_R_", "_L_") - else: - return name + # Create a map to quickly determine the opposite side + swapSides = { + sideInfo["left"]: sideInfo["right"], + sideInfo["right"]: sideInfo["left"] + } + # Get the opposite side + currentSide = matchResult.group("side") + oppositeSide = swapSides.get(currentSide) + if not oppositeSide: # If it's center or an unrecognized side + return name -def swapSideLabelNode(node): - """Returns fliped name of a node + # Replace the matched side with its opposite + swappedName = name[:matchResult.start("side")] + oppositeSide + name[matchResult.end("side"):] + return swappedName - Returns fliped name that replaced side label left to right or - right to left + +def isSideElement(name): + """Returns is name(str) side element? Arguments: - name(node): pyNode + name (str): Description Returns: - str + bool + + Deleted Parameters: + node: str """ - # first check default swapSideLabel. For defaul Shifter naming system - name = node.stripNamespace() - sw_name = swapSideLabel(name) - if name != sw_name: - return sw_name - - # try to find the mirror using custom side labels - if node.hasAttr("side_label"): - side = node.side_label.get() - if side in "LR": - # custom side label - c_side = node.attr("{}_custom_side_label".format(side)).get() - # mirror side label - if side == "L": - cm_side = node.attr("R_custom_side_label").get() - elif side == "R": - cm_side = node.attr("L_custom_side_label").get() - return node.stripNamespace().replace(c_side, cm_side) - else: - return node.stripNamespace() + 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 swapSideLabel(node.stripNamespace()) + return False def getClosestNode(node, nodesToQuery): @@ -1203,20 +1218,21 @@ def spine_FKToIK(fkControls, ikControls, matchMatrix_dict=None): ################################################## -def getMirrorTarget(nameSpace, node): +def getMirrorTarget(nameSpace, node, guideData): """Find target control to apply mirroring. Args: nameSpace (str): Namespace node (PyNode): Node to mirror + guideData(dict): guideData on a rig node Returns: PyNode: Mirror target """ - if isSideElement(node.name()): + if getSideLabelFromCtl(node.name()): nameParts = stripNamespace(node.name()).split("|")[-1] - nameParts = swapSideLabel(nameParts) + nameParts = swapSideLabel(nameParts, guideData) nameTarget = ":".join([nameSpace, nameParts]) return getNode(nameTarget) else: @@ -1229,7 +1245,7 @@ def mirrorPose(flip=False, nodes=None): Args: flip (bool, options): Set the function behaviour to flip - nodes (None, [PyNode]): Controls to mirro/flip the pose + nodes (None, [PyNode]): Controls to mirror/flip the pose """ if nodes is None: nodes = pm.selected() @@ -1239,16 +1255,16 @@ def mirrorPose(flip=False, nodes=None): pm.undoInfo(ock=1) try: - nameSpace = False nameSpace = getNamespace(nodes[0]) + guideData = attribute.get_guide_data_from_rig() mirrorEntries = [] for oSel in nodes: - target = getMirrorTarget(nameSpace, oSel) + target = getMirrorTarget(nameSpace, oSel, guideData) mirrorEntries.extend(calculateMirrorData(oSel, target)) # To flip a pose, do mirroring both ways. - if target not in nodes and flip: + if flip and target not in nodes: mirrorEntries.extend(calculateMirrorData(target, oSel)) for dat in mirrorEntries: @@ -1256,13 +1272,8 @@ def mirrorPose(flip=False, nodes=None): except Exception as e: pm.displayWarning("Flip/Mirror pose fail") - pm.displayWarning( - "If you are using Custom naming rules in controls. " - "It is possible that the name configuration makes hard to track " - "the correct object to mirror for {}".format(oSel.name()) - ) - import traceback + import traceback traceback.print_exc() print(e) @@ -1311,50 +1322,35 @@ def calculateMirrorData(srcNode, targetNode, flip=False): Returns: [{"target": node, "attr": at, "val": flipVal}] """ + def getInversionMultiplier(attrName, node): + """Get the inversion multiplier based on the attribute name.""" + invCheckName = getInvertCheckButtonAttrName(attrName) + if not pm.attributeQuery(invCheckName, node=node, shortName=True, exists=True): + return 1 + return -1 if node.attr(invCheckName).get() else 1 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 + inv = getInversionMultiplier(attrName, srcNode) # if flip enabled record self also if flip: flipVal = targetNode.attr(attrName).get() results.append( - {"target": srcNode, "attr": invAttrName, "val": flipVal * inv} + {"target": srcNode, "attr": attrName, "val": flipVal * inv} ) results.append( { "target": targetNode, - "attr": invAttrName, + "attr": attrName, "val": srcNode.attr(attrName).get() * inv, } ) return results + def calculateMirrorDataRBF(srcNode, targetNode): """Calculate the mirror data diff --git a/release/scripts/mgear/core/attribute.py b/release/scripts/mgear/core/attribute.py index 63f5a4b1..b9c13eb9 100644 --- a/release/scripts/mgear/core/attribute.py +++ b/release/scripts/mgear/core/attribute.py @@ -4,6 +4,7 @@ # GLOBAL ############################################# import collections +import json import mgear import pymel.core as pm import maya.cmds as cmds @@ -1161,6 +1162,23 @@ def smart_reset(*args): # GETTERS ########################################################## +def get_guide_data_from_rig(*args): + """Get guide data from a selected or default rig root + + Returns: + dict: a guide data + """ + rigNode = pm.ls("rig") + if not rigNode or not rigNode[0].hasAttr("is_rig"): + mgear.log( + "Not rig root selected or found.\nSelect the rig root", + mgear.sev_error, + ) + return + if rigNode[0].hasAttr("is_rig"): + guide_dict = rigNode[0].guide_data.get() + return json.loads(guide_dict) + def get_channelBox(): """Get the channel box