Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed the mirroring bug that occurs when using a custom control naming pattern or when side names 'L' and 'R' are changed. #285

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 110 additions & 114 deletions release/scripts/mgear/core/anim_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<component>.*?)_(?P<side>L|R|C)(?P<index>\d*)_(?P<extension>_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<component>.*?)",
side="(?P<side>{})".format("|".join(sides)),
index="(?P<index>\d*)",
description="?(?P<description>.*?)?",
extension="(?P<extension>{})".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):
Expand Down Expand Up @@ -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:
Expand All @@ -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()
Expand All @@ -1239,30 +1255,25 @@ 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:
applyMirror(nameSpace, dat)

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)

Expand Down Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions release/scripts/mgear/core/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# GLOBAL
#############################################
import collections
import json
import mgear
import pymel.core as pm
import maya.cmds as cmds
Expand Down Expand Up @@ -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
Expand Down