How am I supposed to mirror my controls after resizing etc? It doesn’t do anything, I’m using the default Biped Template. See YouTube video: https://youtu.be/bvw4G9DKudQ?si=LFFie8EKDQok_gR3
1 Like
Hopefully an error popping in script editor, could you paste it over?
Hi thanks for the reply! Here’s what Maya’s spitting out:
select -r arm_L0_fk2_ctl ;
# Traceback (most recent call last):
# File "/Users/siebe/Library/Preferences/Autodesk/maya/scripts/mgear/rigbits/mirror_controls.py", line 171, in mirror_button_pressed
# self.func.mirror_left_to_right()
# File "/Users/siebe/Library/Preferences/Autodesk/maya/scripts/mgear/rigbits/mirror_controls.py", line 117, in mirror_left_to_right
# for source in self.get_specific_side_controls(side="L"):
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# File "/Users/siebe/Library/Preferences/Autodesk/maya/scripts/mgear/rigbits/mirror_controls.py", line 50, in get_specific_side_controls
# if node.nodeType() == "objectSet":
# ^^^^^^^^^^^^^
# AttributeError: 'str' object has no attribute 'nodeType'
I got it working by fixing the code, it was a PyMel Python 3 issue. The issue occurred because Maya’s API returned node names as plain text strings rather than PyNode objects. I modified “mirror_controls.py” in the scripts → mGear → rigbits folder into this:
import mgear.pymaya as pm
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
import mgear
from mgear.vendor.Qt import QtCore, QtWidgets
class MirrorController:
def __init__(self):
pass
@staticmethod
def get_opposite_control(node):
try:
side_l = node.attr("L_custom_side_label").get()
side_r = node.attr("R_custom_side_label").get()
side = node.attr("side_label").get()
target_name = None
if side == "L":
target_name = node.name().replace(side_l, side_r)
elif side == "R":
target_name = node.name().replace(side_r, side_l)
if target_name and pm.objExists(target_name):
return pm.PyNode(target_name)
return None
except pm.MayaAttributeError:
# try to get the opposite control using the old logic
target_name = mgear.core.string.convertRLName(node.name())
target = None
if pm.objExists(target_name):
target = pm.PyNode(target_name)
return target
@staticmethod
def get_specific_side_controls(side="L"):
# Find controllers set
rig_models = [item for item in pm.ls(transforms=True) if item.hasAttr("is_rig")]
if not rig_models:
return []
controllers_set = None
for node in rig_models[0].rigGroups.inputs():
if node.name().endswith("controllers_grp"):
controllers_set = node
break
if not controllers_set:
return []
# Collect all transforms from set and subsets, safely converting strings to PyNodes
raw_members = [pm.PyNode(x) if isinstance(x, str) else x for x in controllers_set.members()]
nodes = []
for node in raw_members:
if node.nodeType() == "objectSet":
sub_members = [pm.PyNode(x) if isinstance(x, str) else x for x in node.members()]
nodes.extend(sub_members)
else:
nodes.append(node)
side_name = None
side_ctl = [node for node in nodes if node.hasAttr("{}_custom_side_label".format(side))]
if side_ctl:
side_name = side_ctl[0].attr("{}_custom_side_label".format(side)).get()
# Find all nodes matching side_name
if side_name:
nodes = [x for x in nodes if side_name in x.name()]
else:
# Fallback filter by naming convention if custom side label attribute is missing
nodes = [x for x in nodes if x.name().startswith("{}_".format(side.lower())) or "_{}_".format(side) in x.name()]
return nodes
@staticmethod
def copy_and_prepare_source(source):
source_copy = pm.duplicate(source)[0]
mgear.core.attribute.setKeyableAttributes(
source_copy,
["tx", "ty", "tz", "ro", "rx", "ry", "rz", "sx", "sy", "sz"]
)
for child in source_copy.getChildren():
if child.nodeType() != "nurbsCurve":
pm.delete(child)
return source_copy
def mirror_pairs(self, pairs):
# Modify control shapes
for source, target in pairs:
source_copy = self.copy_and_prepare_source(source)
pm.select(clear=True)
# Mirror a source copy under a group node
grp = pm.group(em=True, world=True)
pm.parent(source_copy, grp)
grp.scaleX.set(-1)
# Re-parent, freeze transforms and match color
pm.parent(source_copy, target)
pm.makeIdentity(source_copy, apply=True, t=1, r=1, s=1, n=0)
pm.parent(source_copy, target.getParent())
target_color = mgear.core.curve.get_color(target)
if target_color:
mgear.core.curve.set_color(source_copy, target_color)
# Replace shape
mgear.rigbits.replaceShape(source_copy, [target])
# Clean up
pm.delete(grp)
pm.delete(source_copy)
def mirror_selection(self):
pairs = []
for source in pm.selected():
target = self.get_opposite_control(source)
if not target:
continue
pairs.append([source, target])
self.mirror_pairs(pairs)
def mirror_left_to_right(self):
pairs = []
for source in self.get_specific_side_controls(side="L"):
target = self.get_opposite_control(source)
if target:
pairs.append([source, target])
self.mirror_pairs(pairs)
def mirror_right_to_left(self):
pairs = []
for source in self.get_specific_side_controls(side="R"):
target = self.get_opposite_control(source)
if target:
pairs.append([source, target])
self.mirror_pairs(pairs)
class MirrorControlsUi(MayaQWidgetDockableMixin, QtWidgets.QDialog):
def __init__(self, parent=None):
super(MirrorControlsUi, self).__init__(parent)
self.func = MirrorController()
self.setWindowTitle("Mirror Controls")
self.setWindowFlags(QtCore.Qt.Window)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose, 1)
self.setMinimumSize(QtCore.QSize(350, 0))
layout = QtWidgets.QVBoxLayout(self)
self.selection_button = QtWidgets.QRadioButton("Selection")
self.selection_button.setChecked(True)
layout.addWidget(self.selection_button)
self.left_to_right_button = QtWidgets.QRadioButton("Left to Right")
layout.addWidget(self.left_to_right_button)
self.right_to_left_button = QtWidgets.QRadioButton("Right to Left")
layout.addWidget(self.right_to_left_button)
self.mirror_button = QtWidgets.QPushButton("Mirror Controls")
layout.addWidget(self.mirror_button)
self.mirror_button.clicked.connect(self.mirror_button_pressed)
def mirror_button_pressed(self):
pm.undoInfo(openChunk=True)
# Store selection
selection = pm.selected()
if self.selection_button.isChecked():
self.func.mirror_selection()
if self.left_to_right_button.isChecked():
self.func.mirror_left_to_right()
if self.right_to_left_button.isChecked():
self.func.mirror_right_to_left()
# Restore selection
pm.select(selection)
pm.undoInfo(closeChunk=True)
def show(*args):
mgear.core.pyqt.showDialog(MirrorControlsUi)
if __name__ == "__main__":
show()
2 Likes