From c758add67dc5a031486a64f6e096fa04aa3779d2 Mon Sep 17 00:00:00 2001 From: Joji Date: Fri, 22 Sep 2023 21:43:54 -0700 Subject: [PATCH 1/4] Added a function to get a guide data from a rig node --- release/scripts/mgear/core/attribute.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/release/scripts/mgear/core/attribute.py b/release/scripts/mgear/core/attribute.py index 63f5a4b1..2295b2a1 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,25 @@ 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 + """ + selection = pm.ls(selection=True) + if not selection: + selection = pm.ls("rig") + if not selection or not selection[0].hasAttr("is_rig"): + mgear.log( + "Not rig root selected or found.\nSelect the rig root", + mgear.sev_error, + ) + return + if selection[0].hasAttr("is_rig"): + guide_dict = selection[0].guide_data.get() + return json.loads(guide_dict) + def get_channelBox(): """Get the channel box From 370991de5d2fb2f5df2e0a34c0505e3a2e5bb140 Mon Sep 17 00:00:00 2001 From: Joji Date: Sat, 23 Sep 2023 10:52:59 -0700 Subject: [PATCH 2/4] deleted unnecessary code --- release/scripts/mgear/core/attribute.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/release/scripts/mgear/core/attribute.py b/release/scripts/mgear/core/attribute.py index 2295b2a1..b9c13eb9 100644 --- a/release/scripts/mgear/core/attribute.py +++ b/release/scripts/mgear/core/attribute.py @@ -1168,17 +1168,15 @@ def get_guide_data_from_rig(*args): Returns: dict: a guide data """ - selection = pm.ls(selection=True) - if not selection: - selection = pm.ls("rig") - if not selection or not selection[0].hasAttr("is_rig"): - mgear.log( - "Not rig root selected or found.\nSelect the rig root", - mgear.sev_error, - ) - return - if selection[0].hasAttr("is_rig"): - guide_dict = selection[0].guide_data.get() + 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) From 543252bcccb22fac9816cb245296f6a3205ffb95 Mon Sep 17 00:00:00 2001 From: Joji Date: Sat, 23 Sep 2023 14:45:03 -0700 Subject: [PATCH 3/4] Swap the side label of a given control name, based on the rig's naming convention It fixes the mirroring issue with custom naming conventions. 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. --- release/scripts/mgear/core/anim_utils.py | 210 +++++++++++------------ 1 file changed, 101 insertions(+), 109 deletions(-) diff --git a/release/scripts/mgear/core/anim_utils.py b/release/scripts/mgear/core/anim_utils.py index a655c91f..23f480c9 100644 --- a/release/scripts/mgear/core/anim_utils.py +++ b/release/scripts/mgear/core/anim_utils.py @@ -47,115 +47,128 @@ # util -def isSideElement(name): - """Returns is name(str) side element? - - Arguments: - name (str): Description +def getCtlRulePattern(): + """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)" """ + 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 + """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" """ + 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): @@ -1229,7 +1242,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,7 +1252,6 @@ def mirrorPose(flip=False, nodes=None): pm.undoInfo(ock=1) try: - nameSpace = False nameSpace = getNamespace(nodes[0]) mirrorEntries = [] @@ -1248,7 +1260,7 @@ def mirrorPose(flip=False, nodes=None): 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 +1268,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 +1318,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 From 010780e4d1d2356d50c505d2ad223a48fc838d60 Mon Sep 17 00:00:00 2001 From: Joji Date: Sun, 24 Sep 2023 10:11:21 -0700 Subject: [PATCH 4/4] Mirror process is a bit faster with avoiding to load guide data every time --- release/scripts/mgear/core/anim_utils.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/release/scripts/mgear/core/anim_utils.py b/release/scripts/mgear/core/anim_utils.py index 23f480c9..76768d6b 100644 --- a/release/scripts/mgear/core/anim_utils.py +++ b/release/scripts/mgear/core/anim_utils.py @@ -47,7 +47,7 @@ # util -def getCtlRulePattern(): +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. @@ -60,7 +60,8 @@ def getCtlRulePattern(): the generated pattern might look something like: "(?P.*?)_(?PL|R|C)(?P\d*)_(?P_ctl)" """ - guideData = attribute.get_guide_data_from_rig() + if not guideData: + guideData = attribute.get_guide_data_from_rig() guideInfo = guideData["guide_root"]["param_values"] # Extract relevant data @@ -97,7 +98,7 @@ def getSideLabelFromCtl(ctl): return side -def swapSideLabel(name): +def swapSideLabel(name, guideData=None): """Swap the side label of a given control name, based on the rig's naming convention. The function uses the guide data's naming patterns to identify the current side label @@ -115,7 +116,8 @@ def swapSideLabel(name): swapSideLabel("arm_L_01_ctl") "arm_R_01_ctl" """ - guideData = attribute.get_guide_data_from_rig() + if not guideData: + guideData = attribute.get_guide_data_from_rig() guideInfo = guideData["guide_root"]["param_values"] sideInfo = { @@ -1216,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: @@ -1253,10 +1256,11 @@ def mirrorPose(flip=False, nodes=None): pm.undoInfo(ock=1) try: 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.