Home Website Youtube GitHub

Mirror Controls is not working

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