# -*- coding: utf-8 -*-
"""大白鹅 Maya 工具箱 —— 单文件整合版

用法：在 Maya 脚本编辑器中执行本文件（或 Source 本脚本）即可弹出工具箱窗口，
鼠标悬浮在按钮上可查看每个功能的使用说明。

如需添加 shelf 按钮，执行：
    import dabaieToolbox_single
    dabaieToolbox_single.install()
"""
from ast import literal_eval
import traceback

import maya.cmds as cmds
import maya.mel as mel
import maya.api.OpenMaya as OM2

try:
    from PySide2 import QtWidgets, QtCore
    from shiboken2 import wrapInstance
except ImportError:
    from PySide import QtGui as QtWidgets
    from PySide import QtCore
    from shiboken import wrapInstance

from maya import OpenMayaUI

try:
    from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
except ImportError:
    try:
        from pymel.core.general import MayaQWidgetDockableMixin
    except ImportError:
        MayaQWidgetDockableMixin = None

# ============================ 基础工具函数 ============================
def get_dag_path(obj):
    """Return MDagPath of the object.

    :param obj: (str): Maya object.
    :return:
        MDagPath: OpenMaya MDagPath of the object.
    """

    sel = OM2.MSelectionList()
    sel.add(obj)

    m_dag = sel.getDagPath(0)

    return m_dag


def get_m_point(obj):
    """Return the 3D point with double-precision coordinates of the object.

    :param obj: (str): Maya object.
    :return:
        MPoint: The 3D point with double-precision coordinates of the object.
    """

    pos = cmds.xform(obj, q=True, translation=True, ws=True)

    return OM2.MPoint(pos[0], pos[1], pos[2], 1.0)
def add_string_info(information, obj, attr=''):
    """Add information attribute.

    :param information: (float/int/ste/tuple/list/dict): Information to add as string value of this attribute.
    :param obj: (str): Object to add string type attribute on.
    :param attr: (str): Attribute name.

    """

    if not cmds.objExists("{}.{}".format(obj, attr)):
        cmds.addAttr(obj, ln=attr, dt="string")

    if not information:
        information = ''

    cmds.setAttr("{}.{}".format(obj, attr), lock=False)
    cmds.setAttr("{}.{}".format(obj, attr), e=True, keyable=True)
    cmds.setAttr("{}.{}".format(obj, attr), information, type="string")
    cmds.setAttr("{}.{}".format(obj, attr), lock=True)


def get_string_info(obj, attr=''):
    """Return value from string type attribute.

    :param obj: (str): Object has this string type attribute.
    :param attr: (str): Attribute name.
    :return: (float/int/str/tuple/list/dict/None): Converted from string information which is queried from attribute.
    """

    if cmds.objExists("{}.{}".format(obj, attr)):
        string_info_message = cmds.getAttr("{}.{}".format(obj, attr))
        if string_info_message:
            info = literal_eval(string_info_message)
            return info

        else:
            return None

    return None


def lock_and_hide_attrs(obj, attrs_list, lock=True, hide=True):
    """

    :param obj: str\\Object which has a list of attributes need to be lock/hide or unlock/show.
    :param attrs_list: list\\A list of attributes need to be lock/hide or unlock/show.
    :param lock: bool\\Lock or unlock the attributes.
    :param hide: bool\\Hide or show the attributes.
    """

    for attr in attrs_list:
        if cmds.objExists("{}.{}".format(obj, attr)):
            if lock:
                cmds.setAttr("{}.{}".format(obj, attr), lock=True)
            if hide:
                cmds.setAttr("{}.{}".format(obj, attr), keyable=False, channelBox=False)
            if not lock:
                cmds.setAttr("{}.{}".format(obj, attr), lock=False)
            if not hide:
                cmds.setAttr("{}.{}".format(obj, attr), keyable=True)
def get_shape(node, intermediate=False):
    """Return geometry shape node.

    :param node: (str): Geometry to query shape node.
    :param intermediate: (bool): Return the geometry's intermediate shape node or not.

    :return:
        str/None: Shape node.
    """

    if cmds.nodeType(node) == "transform":
        shapes = cmds.listRelatives(node, shapes=True, path=True)
        if not shapes:
            shapes = []
        for shape in shapes:
            is_intermediate = cmds.getAttr("{}.intermediateObject".format(shape))
            # sometimes there are left over intermediate shapes that are not used so
            # check the connections to make sure we get the one that is used.
            if intermediate and is_intermediate and cmds.listConnections(shape, source=False):
                return shape
            elif not intermediate and not is_intermediate:
                return shape

        if shapes:
            return shapes[0]

    elif cmds.nodeType(node) in ["mesh", "nurbsCurve", "nurbsSurface", "lattice"]:
        parent_node = cmds.listRelatives(node, parent=True)[0]
        if cmds.nodeType(parent_node) in ["mesh", "nurbsCurve", "nurbsSurface", "lattice"]:
            return parent_node
        return node

    return None
def get_uv_at_closest_point(geo, poj_obj):
    """Return the uv value of the closest point on the geometry (polygon mesh/nurbs surface)
       to the given project object.

    :param geo: (str): GEometry (polygon mesh/nurbs surface) to query closest point.
    :param poj_obj: (str): Project object.
    :return:
        tuple: The uv value of the closest point on the geometry (polygon mesh/nurbs surface)
               to the given project object.
    """

    # works more accurate in cm unit
    unit = cmds.currentUnit(q=True)
    cmds.currentUnit(linear="cm")

    # get the shape node
    geo_shape = shapeUtils.get_shape(node=geo)
    if not geo_shape:
        raise ValueError("No shape connected to {}!".format(geo))

    # get the dag object
    geo_dag = baseUtils.get_dag_path(obj=geo)
    poj_pos = baseUtils.get_m_point(obj=poj_obj)

    uv = None

    # get the shape type
    shape_type = cmds.objectType(geo_shape)
    if shape_type == "mesh":
        # get MFn by shape type
        geo_fn = OM2.MFnMesh(geo_dag)
        # return (u, v, face_id)
        uv = geo_fn.getUVAtPoint(poj_pos, space=OM2.MSpace.kWorld)[:-1]

    elif shape_type == "nurbsSurface":
        geo_fn = OM2.MFnNurbsSurface(geo_dag)
        # because api2.0MFnNurbsSurface.closesrPoint has bug, so using create closestPointOnSurface to query position
        cpos_node = cmds.createNode("closestPointOnSurface", name="{}_closestPointOnSurface".format(geo))
        cmds.connectAttr("{}.worldSpace[0]".format(geo_shape), "{}.inputSurface".format(cpos_node))
        cmds.connectAttr("{}.translate".format(poj_obj), "{}/inPosition".format(cpos_node))
        pos = cmds.getAttr("{}.position".format(cpos_node))[0]

        result_pos = OM2.MPoint(pos[0], pos[1], pos[2], 1.0)

        pu, pv = geo_fn.getParamAtPoint(result_pos, ignoreTrimBoundaries=False, tolerance=0.01, space=OM2.MSpace.kWorld)

        uv = (pu/geo_fn.numSpansInU, pv/geo_fn.numSpansInV)

        cmds.delete(cpos_node)

    # set unit back to original
    cmds.currentUnit(linear=unit)

    return uv
def find_center_pivot(objs_list):
    """
    Return the center position of a list of objects.
    :param objs_list: (list/None), A list of objects to query the center position.
    :return: list, The center position of a list of objects, [center_pos_x, center_pos_y, center_pos_z]
    """

    num_of_obj = len(objs_list)

    pos_x_all = 0
    pos_y_all = 0
    pos_z_all = 0

    if num_of_obj:
        for i in range(num_of_obj):
            pos = cmds.xform(objs_list[i], q=True, ws=True, translation=True)
            pos_x_all += pos[0]
            pos_y_all += pos[1]
            pos_z_all += pos[2]

        center_pos_x = pos_x_all / num_of_obj
        center_pos_y = pos_y_all / num_of_obj
        center_pos_z = pos_z_all / num_of_obj

        return [center_pos_x, center_pos_y, center_pos_z]

    return [pos_x_all, pos_y_all, pos_z_all]


def get_manip_pos():
    """Return the manipulate position.

    :return:
        list: The manipulate position, [pos_x, pos_y, pos_z].
    """

    cmds.setToolTo("Move")
    current_move_mode = cmds.manipMoveContext("Move", q=True, m=True)
    cmds.manipMoveContext("Move", e=True, m=0)
    pos = cmds.manipMoveContext("Move", q=True, p=True)
    cmds.manipMoveContext("Move", e=True, m=current_move_mode)

    return pos


def snap_to_manip(obj):
    """Snap a transform node/ a component to the manipulator position.

    :param obj: (str): A transform object/ a component.
    """

    manip_pos = get_manip_pos()
    cmds.xform(obj, ws=True, t=manip_pos)


def loc_at_manip():
    """Create a locator at manipulator position.

    :return: (str): The locator
    """

    manip_pos = get_manip_pos()
    loc = cmds.spaceLocator(name="manipilatorPos_loc", absolute=True)[0]
    cmds.xform(loc, ws=True, t=manip_pos)

    return loc


def snap_to_center(objs_list, obj):
    """
    Snap a transform node/ a component to the center position of a list of objects.
    :param objs_list: A list of objects to query the center position.
    :param obj: A transform object/ a component.

    """

    exist_list = [obj_node for obj_node in objs_list if cmds.objExists(obj_node)]
    if exist_list:
        center_pos = find_center_pivot(objs_list=exist_list)
        cmds.xform(obj, ws=True, t=center_pos)


def parent_object(parent_obj, child_obj):
    """Parent object'

    :param parent_obj: (str): The parent object.
    :param child_obj: (str): The child object.

    """

    parents = cmds.listRelatives(child_obj, parent=True)
    if parents:
        if parents[0] != parent_obj:
            cmds.parent(child_obj, parent_obj)

        else:
            cmds.parent(child_obj, parent_obj)


def create_follicle(grp_parent, geo, ctrl):
    """Create follicle on the geometry (polygon mesh/nurbs surface) base on controller's position.

    :param grp_parent: (str/None): The group which follicle group is to parent to.
    :param geo: (str): Geometry (polygon mesh/nurbs surface) to create follicle on.
    :param ctrl: (str): Controller which acts like a projecting point to create follicle at closest uv on geometry.
    :return:
        str: The newlt created follicle.

    :raise:
        ValueError: If geometry doesn't hace shape node.
    """

    # get the shape node
    geo_shape = get_shape(node=geo)
    if not geo_shape:
        raise ValueError("No shape connected to {}!".format(geo))

    # query uv
    uv = geoUtils.get_uv_at_closest_point(geo=geo, poj_obj=ctrl)
    if uv:
        # create follicle group and lock the attribute
        follicle_grp = "Follicle_Grp"
        if not cmds.objExists(follicle_grp):
            cmds.group(empty=True, name=follicle_grp)
            cmds.setAttr("{}.v".format(follicle_grp), 0)

            attrUtils.lock_and_hide_attrs(
                obj=follicle_grp,
                attrs_list=["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz", "v"],
                lock=True,
                hide=False
            )

        if grp_parent:
            parent_object(parent_obj=grp_parent, child_obj=follicle_grp)

        # create follicle, if it exists, delete it and create new one.
        follicle_shape = "{}_{}_foliShape".format(geo, ctrl)
        if cmds.objExists(follicle_shape):
            follicle = cmds.listRelatives(follicle_shape, parent=True)[0]
            cmds.delete(follicle, follicle_shape)

        follicle_shape = cmds.createNode("follicle", name="{}_{}_foliShape".format(geo, ctrl))
        follicle = cmds.listRelatives(follicle_shape, parent=True)[0]
        attrUtils.lock_and_hide_attrs(
            obj=follicle, attrs_list=["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz", "v"], lock=True, hide=False
        )

        cmds.setAttr("{}.parameterU".format(follicle_shape), uv[0])
        cmds.setAttr("{}.parameterV".format(follicle_shape), uv[1])
        cmds.parent(follicle, follicle_grp, absolute=True)
        cmds.connectAttr("{}.worldMatrix[0]".format(geo_shape), "{}.inputWorldMatrix".format(follicle_shape))
        cmds.connectAttr("{}.outTranslate".format(follicle_shape), "{}.translate".format(follicle), f=True)
        cmds.connectAttr("{}.outRotate".format(follicle_shape), "{}.rotate".format(follicle), f=True)

        # get the shape type
        shape_type = cmds.objectType(geo_shape)
        # get MFn by shape type
        if shape_type == "mesh":
            cmds.connectAttr("{}.outMesh".format(geo_shape), "{}.inputMesh".format(follicle_shape))

        elif shape_type == "nurbsSurface":
            cmds.connectAttr("{}.local".format(geo_shape), "{}.inputSurface".format(follicle_shape))

        return follicle

    else:
        cmds.warning("{} does not have uv set".format(geo))


def add_face_tag():
    """Add "isFace" tag to selected elements in the hieratchy.

    """

    sel_to_tag_list = cmds.ls(sl=True)

    for sel in sel_to_tag_list:
        if not cmds.objExists("{}.isFace".format(sel)):
            cmds.addAttr(sel, ln="isFace", at="bool", dv=1)
            cmds.setAttr("{}.isFace".format(sel), keyable=False, channelBox=False)

        cmds.setAttr("{}.isFace".format(sel), 1)


def remove_non_face_objs():
    """Remove objects which are not face related.

    """

    assemblies = cmds.ls(assemblies=True) # 搜索所有最上层的transform

    for assembly in assemblies:
        children = cmds.listRelatives(assembly, ad=True, type="transform")
        if children:
            for child in children:
                if not cmds.objExists("{}.isFace".format(child)) or not cmds.getAttr("{}.isFace".format(child)):
                    cmds.delete(child)


def add_face_string():
    """Add a face identical string to objects which are face related,
    so the naming will not conflict when merging with body rig

    """

    assemblies = cmds.ls(assemblies=True)

    for assembly in assemblies:
        if cmds.objExists("{}.isFace".format(assembly)):
            assembly_name_strings = assembly.rpartition("_")
            assembly_face_name = "{}_face_{}".format(assembly_name_strings[0], assembly_name_strings[2])
            assembly = cmds.rename(assembly, assembly_face_name)
            children = cmds.listRelatives(assembly, ad=True, type="transform")
            if children:
                for child in children:
                    if cmds.objExists("{}.isFace".format(child)):
                        child_name_strings = child.rpartition('_')
                        child_face_name = "{}_face_{}".format(child_name_strings[0], child_name_strings[2])
                        cmds.rename(child, child_face_name)


def create_shape_helper(grp_parent, obj):
    """Create a shape helper for maya object(usually joint/controller)'s rotation attributes,
    with "Up", "On", "Left", "Right" attributes can be used Instead.
    It basically converts rotation value to hypotenuse value.

    :param grp_parent: (str/None):  The group which shape helpers group is to parent to.
    :param obj: (str):  Maya object(usually joint/controller) which needs to hace a shape helper.
    :return: tuple:(shape helper sphere grp, shape help aim locator).
            Shape helper sphere grp is used to be point constrained by the object,
            with sphere pointing to the right direction, and orient constrained by the parent of the object.
            Shape help aim locator is used to be parent constrained by the child of the object.
    """

    # set up shape helper group
    shape_helper_grp = "Shape_Helper_Grp"
    if not cmds.objExists(shape_helper_grp):
        cmds.group(empty=True, name=shape_helper_grp)

    attrUtils.lock_and_hide_attrs(
        obj=shape_helper_grp,
        attrs_list=["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz", 'v'],
        lock=True,
        hide=False
    )

    # parent group
    if grp_parent:
        parent_object(parent_obj=grp_parent, child_obj=shape_helper_grp)

    # set up shape helper
    base_name = "{}_shapeHelper".format(obj)

    sh_grp = cmds.group(empty=True, name="{}_grp".format(base_name))
    sh_sphere_grp = cmds.group(empty=True, name="{}_sphere_grp".format(base_name))
    sh_sphere = cmds.sphere(name="{}_sphere".format(base_name), axis=[0, 1, 0],
                            degree=3, ssw=270, esw=450, r=1, nsp=4, s=4, ch=False)[0]
    sh_sphere_shape = cmds.listRelatives(sh_sphere, shapes=True)[0]
    cmds.parent(sh_sphere, sh_sphere_grp, absolute=True)


def get_stretch_value(obj_list):
    """需要两个，注意只能是两个有世界矩阵属性的物体，这个函数可以获取这两个物体的原始距离，并根据他们距离的缩短或伸长来获取拉伸比例，这个比例数值会做
    为属性设置给obj_list中的第一个物体

    :param obj_list: （list）
    """

    # 先创建用于给两个物体xyz坐标分别相减的节点
    pma_1 = cmds.createNode("plusMinusAverage")
    cmds.setAttr(pma_1 + ".operation", 2)

    # 分解两个物体的世界矩阵
    for obj,i in zip(obj_list, range(2)):
        dcpsmtx = cmds.createNode("decomposeMatrix")
        cmds.connectAttr(obj + ".worldMatrix[0]", dcpsmtx + ".inputMatrix")
        cmds.connectAttr(dcpsmtx + ".outputTranslate", pma_1 + ".input3D[{}]".format(i))

    # 把相减的xyz坐标平方
    mult_1 = cmds.createNode("multiplyDivide")
    cmds.setAttr(mult_1 + '.operation', 1)
    cmds.connectAttr(pma_1 + ".output3D", mult_1 + ".input1")
    cmds.connectAttr(pma_1 + ".output3D", mult_1 + ".input2")

    # 把相平方的结果相加
    pma_2 = cmds.createNode("plusMinusAverage")
    cmds.setAttr(pma_2 + ".operation", 1)
    cmds.connectAttr(mult_1 + ".outputX", pma_2 + ".input1D[0]")
    cmds.connectAttr(mult_1 + ".outputY", pma_2 + ".input1D[1]")
    cmds.connectAttr(mult_1 + ".outputZ", pma_2 + ".input1D[2]")

    # 开方
    mult_2 = cmds.createNode("multiplyDivide")
    cmds.setAttr(mult_2 + ".operation", 3)
    cmds.connectAttr(pma_2 + ".output1D", mult_2 + ".input1X")
    cmds.setAttr(mult_2 + ".input2X", 0.5)

    # 除以初始值以获得拉伸比例
    mult_3 = cmds.createNode("multiplyDivide")
    cmds.connectAttr(mult_2 + ".outputX", mult_3 + ".input1X")
    cmds.setAttr(mult_3 + ".operation", 2)
    default_val = cmds.getAttr(mult_3 + ".input1X")
    cmds.setAttr(mult_3 + ".input2X", default_val)
    print(cmds.getAttr(mult_3 + ".outputX"))

    # 将拉伸比例连接到obj_list[0]的新属性上
    cmds.addAttr(obj_list[0], ln="tensileRatio", at="double", keyable=True)
    cmds.connectAttr(mult_3 + ".outputX", obj_list[0] + ".tensileRatio")
def create_jnt(
        prefix, name, side, index,
        translation, rotation, scale,
        orient, sec_axis_orient, preferred_angles,
        jnts_set
):
    """Create jnt based on arguments, prefix_side_name_index, then set attributes values

    :param prefix: (str): Joint's name prefix.
    :param name: (str): Joint's name key word.
    :param side: (str/None): Joint's name side.
    :param index: (int/None): Joint's name index.
    :param translation: (tuple): Joint's world translation position.
    :param rotation: (tuple): Joint's world rotation position.
    :param scale: (tuple): Joint's scale position.
    :param orient: (str): Joint's orientation axis 'xyz', 'yzx', 'zxy', 'zyx', 'yxz', 'xzy', 'none'.
    :param sec_axis_orient: (str): Joint's secondary orientation axis "xup", "xdown", "yup", "ydown", "zup", "zdown",
    "none".
    :param preferred_angles: (tuple): Joint's preferred angles in three axis.
    :param jnts_set: (str/None): Joints set.

    :raise:
        ValueError: If joint name already exists.

    :return:
        str: The newly created joint
    """

    # get side
    if side and side.lower() != "none":
        side = "{}_".format(side)
    else:
        side = ""

    # get index
    if type(index) == int:
        index = "_{:03d}".format(index)
    else:
        index = ""

    jnt_name = "{}_{}{}{}".format(prefix, side, name, index)

    # clean up selection, so joint is not parented to anything.
    cmds.select(clear=True)

    if cmds.objExists(jnt_name):
        raise ValueError("{} already exists in the scene".format(jnt_name))

    jnt = cmds.joint(name=jnt_name)
    cmds.joint(jnt, edit=True, oj=orient, sao=sec_axis_orient, ch=True, zso=True)
    cmds.xform(jnt, scale=scale)
    cmds.xform(jnt, ws=True, a=True, translation=translation, rotation=rotation)
    cmds.setAttr("{}.preferredAngle".format(jnt), preferred_angles[0], preferred_angles[1], preferred_angles[2])

    # parent to set
    if jnts_set:
        joints_set = jnts_set
        if not cmds.objExists(jnts_set) or cmds.nodeType(jnts_set) != "objectSet":
            joints_set = cmds.sets(name=jnts_set, empty=True)

        cmds.sets(jnt, edit=True, forceElement=joints_set)

    return jnt
def get_ctrl_shape_pos(shape):
    """Query controller curve cvs positions from controllers shapes dictionary.

    Args:
        shape (str): Controller shape to query controller curve cvs positions.

    Returns:
        list/None: Controller Curve cvs positions.

    """

    ctrls_shapes_dict = {
        'Arrow1Way': [(8.157751740789365e-08, -0.5009345906086866, -0.25046753747675954), (-1.1122992526757025e-16, -0.5009350499874775, 0.25046752499373875), (1.1122992526757025e-16, 0.5009350499874775, 0.25046752499373875), (1.1122992526757025e-16, 0.5009350499874775, 0.5009350499874775), (2.224598505351405e-16, 1.001870099974955, 0.0), (1.1122992526757025e-16, 0.5009350499874775, -0.5009350499874775), (1.1122992526757025e-16, 0.5009350499874775, -0.25046752499373875), (-1.1122992526757025e-16, -0.5009350499874775, -0.25046752499373875)],
        'Arrow2Way': [(-1.001870099974955, 0.0, 0.0), (-0.50093504998747751, 0.0, 0.50093504998747751), (-0.50093504998747751, 0.0, 0.25046752499373875), (0.50093504998747751, 0.0, 0.25046752499373875), (0.50093504998747751, 0.0, 0.50093504998747751), (1.001870099974955, 0.0, 0.0), (0.50093504998747751, 0.0, -0.50093504998747751), (0.50093504998747751, 0.0, -0.25046752499373875), (-0.50093504998747751, 0.0, -0.25046752499373875), (-0.50093504998747751, 0.0, -0.50093504998747751), (-1.001870099974955, 0.0, 0.0)],
        'Arrow4Way': [(0.0, 0.0, -1.0025507571980443), (0.33418358573268248, 0.0, -0.66836717146536495), (0.16709179286634124, 0.0, -0.66836717146536495), (0.16709179286634124, 0.0, -0.16709179286634124), (0.66836717146536495, 0.0, -0.16709179286634124), (0.66836717146536495, 0.0, -0.33418358573268248), (1.0025507571980443, 0.0, 0.0), (0.66836717146536495, 0.0, 0.33418358573268248), (0.66836717146536495, 0.0, 0.16709179286634124), (0.16709179286634124, 0.0, 0.16709179286634124), (0.16709179286634124, 0.0, 0.66836717146536495), (0.33418358573268248, 0.0, 0.66836717146536495), (0.0, 0.0, 1.0025507571980443), (-0.33418358573268248, 0.0, 0.66836717146536495), (-0.16709179286634124, 0.0, 0.66836717146536495), (-0.16709179286634124, 0.0, 0.16709179286634124), (-0.66836717146536495, 0.0, 0.16709179286634124), (-0.66836717146536495, 0.0, 0.33418358573268248), (-1.0025507571980443, 0.0, 0.0), (-0.66836717146536495, 0.0, -0.33418358573268248), (-0.66836717146536495, 0.0, -0.16709179286634124), (-0.16709179286634124, 0.0, -0.16709179286634124), (-0.16709179286634124, 0.0, -0.66836717146536495), (-0.33418358573268248, 0.0, -0.66836717146536495), (0.0, 0.0, -1.0025507571980443)],
        'BeveledBox': [(0.5, 0.49500051140785217, 0.49500051140785217), (0.49500051140785217, 0.5, 0.49500051140785217), (0.49500051140785217, 0.5, -0.49500051140785217), (0.5, 0.49500051140785217, -0.49500051140785217), (0.5, 0.49500051140785217, 0.49500051140785217), (0.5, -0.49500051140785217, 0.49500051140785217), (0.5, -0.49500051140785217, -0.49500051140785217), (0.5, 0.49500051140785217, -0.49500051140785217), (0.49500051140785217, 0.49500051140785217, -0.5), (0.49500051140785217, -0.49500051140785217, -0.5), (0.5, -0.49500051140785217, -0.49500051140785217), (0.49500051140785217, -0.5, -0.49500051140785217), (0.49500051140785217, -0.5, 0.49500051140785217), (0.5, -0.49500051140785217, 0.49500051140785217), (0.49500051140785217, -0.49500051140785217, 0.5), (0.49500051140785217, 0.49500051140785217, 0.5), (0.5, 0.49500051140785217, 0.49500051140785217), (0.49500051140785217, 0.5, 0.49500051140785217), (0.49500051140785217, 0.49500051140785217, 0.5), (-0.49500051140785217, 0.49500051140785217, 0.5), (-0.49500051140785217, -0.49500051140785217, 0.5), (0.49500051140785217, -0.49500051140785217, 0.5), (0.49500051140785217, -0.5, 0.49500051140785217), (-0.49500051140785217, -0.5, 0.49500051140785217), (-0.49500051140785217, -0.49500051140785217, 0.5), (-0.5, -0.49500051140785217, 0.49500051140785217), (-0.5, 0.49500051140785217, 0.49500051140785217), (-0.49500051140785217, 0.49500051140785217, 0.5), (-0.49500051140785217, 0.5, 0.49500051140785217), (-0.5, 0.49500051140785217, 0.49500051140785217), (-0.5, 0.49500051140785217, -0.49500051140785217), (-0.5, -0.49500051140785217, -0.49500051140785217), (-0.5, -0.49500051140785217, 0.49500051140785217), (-0.49500051140785217, -0.5, 0.49500051140785217), (-0.49500051140785217, -0.5, -0.49500051140785217), (-0.5, -0.49500051140785217, -0.49500051140785217), (-0.49500051140785217, -0.49500051140785217, -0.5), (-0.49500051140785217, -0.5, -0.49500051140785217), (0.49500051140785217, -0.5, -0.49500051140785217), (0.49500051140785217, -0.49500051140785217, -0.5), (-0.49500051140785217, -0.49500051140785217, -0.5), (-0.49500051140785217, 0.49500051140785217, -0.5), (-0.5, 0.49500051140785217, -0.49500051140785217), (-0.49500051140785217, 0.5, -0.49500051140785217), (0.49500051140785217, 0.5, -0.49500051140785217), (0.49500051140785217, 0.49500051140785217, -0.5), (-0.49500051140785217, 0.49500051140785217, -0.5), (-0.49500051140785217, 0.5, -0.49500051140785217), (-0.49500051140785217, 0.5, 0.49500051140785217), (0.49500051140785217, 0.5, 0.49500051140785217)],
        'Circle': [(0.39264623741499377, 5.804408661713559e-17, -0.9479318715820454), (2.432377081290716e-16, 6.28264666274837e-17, -1.0260340642089774), (-0.3926462374149944, 5.804408661713559e-17, -0.9479318715820454), (-0.7255156445305615, 4.442502059028405e-17, -0.7255156445305614), (-0.9479318715820455, 2.404264789237622e-17, -0.39264623741499427), (-1.0260340642089776, -2.8980332155736397e-33, 4.732847409704354e-17), (-0.9479318715820452, -2.4042647892376227e-17, 0.3926462374149943), (-0.7255156445305615, -4.4425020590284066e-17, 0.7255156445305617), (-0.3926462374149942, -5.80440866171356e-17, 0.9479318715820456), (7.753705712792092e-17, -6.28264666274837e-17, 1.0260340642089774), (0.39264623741499455, -5.804408661713559e-17, 0.9479318715820454), (0.7255156445305614, -4.442502059028403e-17, 0.7255156445305612), (0.9479318715820458, -2.404264789237621e-17, 0.39264623741499405), (1.0260340642089771, 1.0793273227319641e-32, -1.7626752847979251e-16), (0.9479318715820455, 2.4042647892376246e-17, -0.39264623741499466), (0.7255156445305612, 4.442502059028406e-17, -0.7255156445305616), (0.39264623741499377, 5.804408661713559e-17, -0.9479318715820454)],
        'Cube': [(1.0000000000000009, 1.0000000000000002, -1.0000000000000002), (1.0000000000000009, 1.0000000000000002, 1.0000000000000002), (-1.0000000000000009, 1.0000000000000002, 1.0000000000000002), (-1.0000000000000009, 1.0000000000000002, -1.0000000000000002), (1.0000000000000009, 1.0000000000000002, -1.0000000000000002), (1.0000000000000009, -1.0000000000000002, -1.0000000000000002), (-1.0000000000000009, -1.0000000000000002, -1.0000000000000002), (-1.0000000000000009, 1.0000000000000002, -1.0000000000000002), (-1.0000000000000009, 1.0000000000000002, 1.0000000000000002), (-1.0000000000000009, -1.0000000000000002, 1.0000000000000002), (-1.0000000000000009, -1.0000000000000002, -1.0000000000000002), (-1.0000000000000009, -1.0000000000000002, 1.0000000000000002), (1.0000000000000009, -1.0000000000000002, 1.0000000000000002), (1.0000000000000009, 1.0000000000000002, 1.0000000000000002), (1.0000000000000009, 1.0000000000000002, -1.0000000000000002), (1.0000000000000009, -1.0000000000000002, -1.0000000000000002), (1.0000000000000009, -1.0000000000000002, 1.0000000000000002)],
        'Diamond': [(0.0, 0.99204443169118106, 0.0), (0.0, 0.0, -0.99204443169118106), (0.0, 0.0, 0.99204443169118106), (0.0, 0.99204443169118106, 0.0), (-0.99204443169118106, 0.0, 0.0), (0.99204443169118106, 0.0, 0.0), (0.0, 0.99204443169118106, 0.0), (-0.99204443169118106, 0.0, 0.0), (0.0, -0.99204443169118106, 0.0), (0.99204443169118106, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, -0.99204443169118106), (0.0, 0.0, 0.99204443169118106), (0.0, -0.99204443169118106, 0.0), (0.0, 0.0, -0.99204443169118106), (0.0, 0.0, 0.0), (0.0, 0.99204443169118106, 0.0), (0.0, -0.99204443169118106, 0.0), (0.0, 0.0, 0.0), (-0.99204443169118106, 0.0, 0.0), (0.0, 0.0, 0.99204443169118106), (0.99204443169118106, 0.0, 0.0), (0.0, 0.0, -0.99204443169118106), (-0.99204443169118106, 0.0, 0.0)],
        'Eye': [(1.013278e-06, 0.0, -2.0), (0.765368, 0.0, -1.847758), (1.414214, 0.0, -1.414212), (1.84776, 0.0, -0.765366), (2.0, 0.0, 0.0), (1.84776, 0.0, 0.765366), (1.414214, 0.0, 1.414214), (0.765366, 0.0, 1.84776), (-2.98024e-07, 0.0, 2.0), (-0.765368, 0.0, 1.847758), (-1.414214, 0.0, 1.414214), (-1.84776, 0.0, 0.765366), (-2.0, 0.0, -7.15256e-07), (-1.847758, 0.0, -0.765368), (-1.414212, 0.0, -1.414214), (-0.765366, 0.0, -1.84776), (1.013278e-06, 0.0, -2.0), (-2.98024e-07, 0.0, 2.0), (0.0, 0.0, 0.0), (2.0, 0.0, 0.0), (-2.0, 0.0, -7.15256e-07)],
        'HalfCircleDown': [(1.969658, 7.911925559156429e-17, -0.35632145), (1.902114, -1.5574463940737357e-17, 0.07014114999999999), (1.618034, -1.3937232479221962e-16, 0.62767715), (1.17557, -2.3761906886576866e-16, 1.07014115), (0.618034, -3.006975002328716e-16, 1.35422115), (-5.96046e-08, -3.224325584305632e-16, 1.45210715), (-0.618034, -3.006975002328716e-16, 1.35422115), (-1.17557, -2.3761906886576866e-16, 1.07014115), (-1.618034, -1.3937232479221962e-16, 0.62767715), (-1.902114, -1.5574463940737357e-17, 0.07014114999999999), (-1.969658, 7.911925559156429e-17, -0.35632145), (1.969658, 7.911925559156429e-17, -0.35632145)],
        'HalfCircleUp': [(1.969658, -7.911925559156429e-17, 0.35632145), (1.902114, 1.5574463940737357e-17, -0.07014114999999999), (1.618036, 1.3937276888142948e-16, -0.6276791500000001), (1.175572, 2.3761906886576866e-16, -1.07014115), (0.618034, 3.006975002328716e-16, -1.35422115), (0.0, 3.224325584305632e-16, -1.45210715), (-0.618034, 3.006975002328716e-16, -1.35422115), (-1.17557, 2.3761906886576866e-16, -1.07014115), (-1.618034, 1.3937232479221962e-16, -0.62767715), (-1.902114, 1.5574463940737357e-17, -0.07014114999999999), (-1.969658, -7.911925559156429e-17, 0.35632145), (1.969658, -7.911925559156429e-17, 0.35632145)],
        'Locator': [(-1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, -1.0, 0.0), (0.0, 0.0, 0.0), (0.0, 0.0, -1.0)],
        'LocatorThick': [(0.090602890444299145, 0.090602890444299145, 0.090602890444299145), (0.99663179488729026, 0.090602890444299145, 0.090602890444299145), (0.99663179488729026, 0.090602890444299145, -0.090602890444299145), (0.99663179488729026, -0.090602890444299145, -0.090602890444299145), (0.99663179488729026, -0.090602890444299145, 0.090602890444299145), (0.99663179488729026, 0.090602890444299145, 0.090602890444299145), (0.99663179488729026, -0.090602890444299145, 0.090602890444299145), (0.090602890444299145, -0.090602890444299145, 0.090602890444299145), (0.090602890444299145, -0.99663179488729026, 0.090602890444299145), (0.090602890444299145, -0.99663179488729026, -0.090602890444299145), (-0.090602890444299145, -0.99663179488729026, -0.090602890444299145), (-0.090602890444299145, -0.99663179488729026, 0.090602890444299145), (0.090602890444299145, -0.99663179488729026, 0.090602890444299145), (0.090602890444299145, -0.99663179488729026, -0.090602890444299145), (0.090602890444299145, -0.090602890444299145, -0.090602890444299145), (0.090602890444299145, -0.090602890444299145, 0.090602890444299145), (0.090602890444299145, -0.090602890444299145, 0.99663179488729026), (0.090602890444299145, 0.090602890444299145, 0.99663179488729026), (0.090602890444299145, 0.090602890444299145, 0.090602890444299145), (0.090602890444299145, -0.090602890444299145, 0.090602890444299145), (-0.090602890444299145, -0.090602890444299145, 0.090602890444299145), (-0.090602890444299145, -0.99663179488729026, 0.090602890444299145), (-0.090602890444299145, -0.99663179488729026, -0.090602890444299145), (-0.090602890444299145, -0.090602890444299145, -0.090602890444299145), (-0.090602890444299145, -0.090602890444299145, 0.090602890444299145), (-0.090602890444299145, -0.090602890444299145, 0.99663179488729026), (0.090602890444299145, -0.090602890444299145, 0.99663179488729026), (0.090602890444299145, 0.090602890444299145, 0.99663179488729026), (-0.090602890444299145, 0.090602890444299145, 0.99663179488729026), (-0.090602890444299145, -0.090602890444299145, 0.99663179488729026), (-0.090602890444299145, 0.090602890444299145, 0.99663179488729026), (-0.090602890444299145, 0.090602890444299145, 0.090602890444299145), (-0.090602890444299145, -0.090602890444299145, 0.090602890444299145), (-0.99663179488729026, -0.090602890444299145, 0.090602890444299145), (-0.99663179488729026, 0.090602890444299145, 0.090602890444299145), (-0.99663179488729026, 0.090602890444299145, -0.090602890444299145), (-0.99663179488729026, -0.090602890444299145, -0.090602890444299145), (-0.99663179488729026, -0.090602890444299145, 0.090602890444299145), (-0.99663179488729026, -0.090602890444299145, -0.090602890444299145), (-0.090602890444299145, -0.090602890444299145, -0.090602890444299145), (0.090602890444299145, -0.090602890444299145, -0.090602890444299145), (0.090602890444299145, -0.090602890444299145, -0.99663179488729026), (-0.090602890444299145, -0.090602890444299145, -0.99663179488729026), (-0.090602890444299145, 0.090602890444299145, -0.99663179488729026), (0.090602890444299145, 0.090602890444299145, -0.99663179488729026), (0.090602890444299145, -0.090602890444299145, -0.99663179488729026), (-0.090602890444299145, -0.090602890444299145, -0.99663179488729026), (-0.090602890444299145, -0.090602890444299145, -0.090602890444299145), (-0.090602890444299145, 0.090602890444299145, -0.090602890444299145), (-0.090602890444299145, 0.090602890444299145, -0.99663179488729026), (-0.090602890444299145, 0.090602890444299145, -0.090602890444299145), (0.090602890444299145, 0.090602890444299145, -0.090602890444299145), (0.090602890444299145, 0.090602890444299145, -0.99663179488729026), (0.090602890444299145, 0.090602890444299145, -0.090602890444299145), (0.090602890444299145, -0.090602890444299145, -0.090602890444299145), (0.99663179488729026, -0.090602890444299145, -0.090602890444299145), (0.99663179488729026, 0.090602890444299145, -0.090602890444299145), (0.090602890444299145, 0.090602890444299145, -0.090602890444299145), (0.090602890444299145, 0.090602890444299145, 0.090602890444299145), (0.090602890444299145, 0.99663179488729026, 0.090602890444299145), (0.090602890444299145, 0.99663179488729026, -0.090602890444299145), (0.090602890444299145, 0.090602890444299145, -0.090602890444299145), (-0.090602890444299145, 0.090602890444299145, -0.090602890444299145), (-0.090602890444299145, 0.99663179488729026, -0.090602890444299145), (0.090602890444299145, 0.99663179488729026, -0.090602890444299145), (-0.090602890444299145, 0.99663179488729026, -0.090602890444299145), (-0.090602890444299145, 0.99663179488729026, 0.090602890444299145), (0.090602890444299145, 0.99663179488729026, 0.090602890444299145), (-0.090602890444299145, 0.99663179488729026, 0.090602890444299145), (-0.090602890444299145, 0.090602890444299145, 0.090602890444299145), (0.090602890444299145, 0.090602890444299145, 0.090602890444299145), (-0.090602890444299145, 0.090602890444299145, 0.090602890444299145), (-0.99663179488729026, 0.090602890444299145, 0.090602890444299145), (-0.99663179488729026, 0.090602890444299145, -0.090602890444299145), (-0.090602890444299145, 0.090602890444299145, -0.090602890444299145), (-0.090602890444299145, 0.090602890444299145, 0.090602890444299145)],
        'Pelvis': [(-4.4996484129376775, 0.28104040703785677, -4.4996484129376775), (4.4996484129376775, 0.28104040703785677, -4.4996484129376775), (2.287271355626842, -4.81634468751716, -2.287271355626842), (-2.287271355626842, -4.81634468751716, -2.287271355626842), (-4.4996484129376775, 0.28104040703785677, -4.4996484129376775), (-4.4996484129376775, 0.28104040703785677, 4.4996484129376775), (-2.287271355626842, -4.81634468751716, 2.287271355626842), (-2.287271355626842, -4.81634468751716, -2.287271355626842), (-2.287271355626842, -4.81634468751716, 2.287271355626842), (2.287271355626842, -4.81634468751716, 2.287271355626842), (4.4996484129376775, 0.28104040703785677, 4.4996484129376775), (-4.4996484129376775, 0.28104040703785677, 4.4996484129376775), (4.4996484129376775, 0.28104040703785677, 4.4996484129376775), (4.4996484129376775, 0.28104040703785677, -4.4996484129376775), (2.287271355626842, -4.81634468751716, -2.287271355626842), (2.287271355626842, -4.81634468751716, 2.287271355626842), (4.4996484129376775, 0.28104040703785677, 4.4996484129376775)],
        'Pin': [(0.0, 0.0, 0.0), (-8.970377550366765e-08, 3.3198874323143087e-16, -1.4951444700120469), (-0.09598856807868952, 3.1491461005559387e-16, -1.5142378885785872), (-0.17736353860735718, 3.089189987764206e-16, -1.5686110263845117), (0.17736353860735718, 4.664494105137109e-16, -1.9233378527693787), (0.09598831724884563, 4.604537435391237e-16, -1.9777109905753019), (0.0, 4.433795904450483e-16, -1.9968041583120026), (-0.09598831724884563, 4.1782636757724673e-16, -1.9777109905753019), (-0.17736328777751303, 3.876842324927722e-16, -1.9233378527693787), (0.17736353860735718, 3.876841767973585e-16, -1.568610775554664), (0.23173642558343618, 4.178262561864203e-16, -1.6499857460833356), (0.25082984414997667, 4.433795904450478e-16, -1.7459743141620239), (0.23173667641328016, 4.604537435391237e-16, -1.8419626314108672), (0.17736353860735718, 4.664494105137109e-16, -1.9233378527693787), (-0.17736353860735718, 3.089189987764206e-16, -1.5686110263845117), (-0.23173667641328016, 3.1491461005559397e-16, -1.6499859969131783), (-0.25082984414997667, 3.319888188450836e-16, -1.745974564991866), (-0.23173642558343618, 3.57542097408298e-16, -1.8419628822407126), (-0.17736328777751303, 3.876842324927722e-16, -1.9233378527693787), (0.17736353860735718, 3.876841767973585e-16, -1.568610775554664), (0.09598831724884563, 3.5754198601747055e-16, -1.5142376377487419), (-8.970377550366765e-08, 3.3198874323143087e-16, -1.4951444700120469)],
        'Pin2x': [(-1.2207543780853928e-07, -3.107136738025877e-13, 4.095770838597601), (0.29416010388603464, -3.107136738025877e-13, 4.191349266621592), (0.4759610776345259, -3.107136738025877e-13, 4.441576939786146), (0.47596108025675155, -3.107136738025877e-13, 4.750875235170268), (0.294160119738282, -3.107136738025877e-13, 5.001102827323976), (-4.312727308536028e-08, -3.107136738025877e-13, 5.09668128945062), (-0.29416024568619054, -3.107136738025877e-13, 5.00110288518722), (-0.47596125773805575, -3.107136738025877e-13, 4.750875287197268), (-0.4759612979475843, -3.107136738025877e-13, 4.441576972661457), (-0.2941603182774276, -3.107136738025877e-13, 4.191349342920449), (-1.2207543780853928e-07, -3.107136738025877e-13, 4.095770838597601), (-7.619179162032808e-08, -1.6730736281677798e-13, -0.0037739159751097873), (-4.3824592487089704e-08, -3.107136738025877e-13, -4.108628920497032), (-0.2910389832725113, -3.107136738025877e-13, -4.203193165316563), (-0.4709109485412854, -3.107136738025877e-13, -4.450765658488912), (-0.4709109883241603, -3.107136738025877e-13, -4.7567820830509735), (-0.29103905509350037, -3.107136738025877e-13, -5.004354607637477), (-1.2193505723560214e-07, -3.107136738025877e-13, -5.09891895145475), (0.29103884066709773, -3.107136738025877e-13, -5.004354683126752), (0.47091076803892545, -3.107136738025877e-13, -4.756782115577454), (0.4709107706333276, -3.107136738025877e-13, -4.450765709963866), (0.2910388563511408, -3.107136738025877e-13, -4.203193222565831), (-4.3824592487089704e-08, -3.107136738025877e-13, -4.108628920497032)],
        'PointyLocal': [(0.96546423959374483, 0.0, 0.002170028206335372), (0.96546423959374483, 0.0, -0.002170028206335372), (0.90041776784158323, 0.0, -0.02318291233311584), (0.75578472890821535, 0.0, -0.096139258282124093), (0.69503976950695012, 0.0, -0.1439620089383836), (0.66428416628109677, 0.0, -0.18015666247163734), (0.65962192896604765, 0.0, -0.19426546974552469), (0.65513807688078685, 0.0, -0.1980743373312277), (0.65021978539453773, 0.0, -0.17785860100078946), (0.66316757484101163, 0.0, -0.15320794957919995), (0.67833390314290287, 0.0, -0.12728061107139119), (0.68991009739336917, 0.0, -0.10639223822848658), (0.69246847648566034, 0.0, -0.072726984683688031), (0.60853294289866433, 0.0, -0.072726984683688031), (0.5968368777282842, 0.0, -0.15992189508793433), (0.5351093475185269, 0.0, -0.30894542272512288), (0.436914982640351, 0.0, -0.43691485906208233), (0.30894554630339083, 0.0, -0.53510928572939276), (0.15992201866620212, 0.0, -0.59683681593915006), (0.072726984683688031, 0.0, -0.6084709683972862), (0.072726984683688031, 0.0, -0.69246847648566034), (0.10639223822848658, 0.0, -0.68991009739336917), (0.12728061107139119, 0.0, -0.67833390314290287), (0.15320794957919995, 0.0, -0.66316757484101163), (0.17785860100078946, 0.0, -0.65021978539453773), (0.1980743373312277, 0.0, -0.65513807688078685), (0.19426546974552469, 0.0, -0.65962192896604765), (0.18015666247163734, 0.0, -0.66428416628109677), (0.1439620089383836, 0.0, -0.69503976950695012), (0.096139258282124093, 0.0, -0.75578472890821535), (0.02318291233311584, 0.0, -0.90041776784158323), (0.002170028206335372, 0.0, -0.96546423959374483), (-0.002170028206335372, 0.0, -0.96546423959374483), (-0.02318291233311584, 0.0, -0.90041776784158323), (-0.096139258282124093, 0.0, -0.75578472890821535), (-0.1439620089383836, 0.0, -0.69503976950695012), (-0.18015666247163734, 0.0, -0.66428416628109677), (-0.19426546974552469, 0.0, -0.65962192896604765), (-0.1980743373312277, 0.0, -0.65513807688078685), (-0.17785860100078946, 0.0, -0.65021978539453773), (-0.15320794957919995, 0.0, -0.66316757484101163), (-0.12728061107139119, 0.0, -0.67833390314290287), (-0.10639223822848658, 0.0, -0.68991009739336917), (-0.072726984683688031, 0.0, -0.69246847648566034), (-0.072726984683688031, 0.0, -0.6084709683972862), (-0.15992189508793433, 0.0, -0.59683693951741856), (-0.30894548451425718, 0.0, -0.53510940930766149), (-0.436914982640351, 0.0, -0.4369150444294842), (-0.53510940930766149, 0.0, -0.30894560809252491), (-0.59683700130655237, 0.0, -0.15992201866620212), (-0.60816189914913743, 0.0, -0.072726984683688031), (-0.69246847648566034, 0.0, -0.072726984683688031), (-0.68991009739336917, 0.0, -0.10639223822848658), (-0.67833390314290287, 0.0, -0.12728061107139119), (-0.66316757484101163, 0.0, -0.15320794957919995), (-0.65021978539453773, 0.0, -0.17785860100078946), (-0.65513807688078685, 0.0, -0.1980743373312277), (-0.65962192896604765, 0.0, -0.19426546974552469), (-0.66428416628109677, 0.0, -0.18015666247163734), (-0.69503976950695012, 0.0, -0.1439620089383836), (-0.75578472890821535, 0.0, -0.096139258282124093), (-0.90041776784158323, 0.0, -0.02318291233311584), (-0.96546423959374483, 0.0, -0.002170028206335372), (-0.96546423959374483, 0.0, 0.002170028206335372), (-0.90041776784158323, 0.0, 0.02318291233311584), (-0.75578472890821535, 0.0, 0.096139258282124093), (-0.69503976950695012, 0.0, 0.1439620089383836), (-0.66428416628109677, 0.0, 0.18015666247163734), (-0.65962192896604765, 0.0, 0.19426546974552469), (-0.65513807688078685, 0.0, 0.1980743373312277), (-0.65021978539453773, 0.0, 0.17785860100078946), (-0.66316757484101163, 0.0, 0.15320794957919995), (-0.67833390314290287, 0.0, 0.12728061107139119), (-0.68991009739336917, 0.0, 0.10639223822848658), (-0.69246847648566034, 0.0, 0.072726984683688031), (-0.60816189914913743, 0.0, 0.072726984683688031), (-0.59683706309568663, 0.0, 0.15992195687706803), (-0.53510947109679541, 0.0, 0.30894554630339083), (-0.4369150444294842, 0.0, 0.4369150444294842), (-0.30894560809252491, 0.0, 0.53510947109679541), (-0.15992201866620212, 0.0, 0.59683706309568663), (-0.072726984683688031, 0.0, 0.60686877615327939), (-0.072726984683688031, 0.0, 0.69246847648566034), (-0.10639223822848658, 0.0, 0.68991009739336917), (-0.12728061107139119, 0.0, 0.67833390314290287), (-0.15320794957919995, 0.0, 0.66316757484101163), (-0.17785860100078946, 0.0, 0.65021978539453773), (-0.1980743373312277, 0.0, 0.65513807688078685), (-0.19426546974552469, 0.0, 0.65962192896604765), (-0.18015666247163734, 0.0, 0.66428416628109677), (-0.1439620089383836, 0.0, 0.69503976950695012), (-0.096139258282124093, 0.0, 0.75578472890821535), (-0.02318291233311584, 0.0, 0.90041776784158323), (-0.002170028206335372, 0.0, 0.96546423959374483), (0.002170028206335372, 0.0, 0.96546423959374483), (0.02318291233311584, 0.0, 0.90041776784158323), (0.096139258282124093, 0.0, 0.75578472890821535), (0.1439620089383836, 0.0, 0.69503976950695012), (0.18015666247163734, 0.0, 0.66428416628109677), (0.19426546974552469, 0.0, 0.65962192896604765), (0.1980743373312277, 0.0, 0.65513807688078685), (0.17785860100078946, 0.0, 0.65021978539453773), (0.15320794957919995, 0.0, 0.66316757484101163), (0.12728061107139119, 0.0, 0.67833390314290287), (0.10639223822848658, 0.0, 0.68991009739336917), (0.072726984683688031, 0.0, 0.69246847648566034), (0.072726984683688031, 0.0, 0.60686877615327939), (0.15992201866620212, 0.0, 0.59683712488482055), (0.30894560809252491, 0.0, 0.53510953288592944), (0.43691510621861845, 0.0, 0.43691516800775232), (0.53510959467506236, 0.0, 0.30894566988165872), (0.59683718667395425, 0.0, 0.15992201866620212), (0.60853294289866433, 0.0, 0.072726984683688031), (0.69246847648566034, 0.0, 0.072726984683688031), (0.68991009739336917, 0.0, 0.10639223822848658), (0.67833390314290287, 0.0, 0.12728061107139119), (0.66316757484101163, 0.0, 0.15320794957919995), (0.65021978539453773, 0.0, 0.17785860100078946), (0.65513807688078685, 0.0, 0.1980743373312277), (0.65962192896604765, 0.0, 0.19426546974552469), (0.66428416628109677, 0.0, 0.18015666247163734), (0.69503976950695012, 0.0, 0.1439620089383836), (0.75578472890821535, 0.0, 0.096139258282124093), (0.90041776784158323, 0.0, 0.02318291233311584), (0.96546423959374483, 0.0, 0.002170028206335372)],
        'Pyramid': [(6.014278109928377e-15, 0.8581964230189217, -6.0142781099283556e-15), (6.014278109928377e-15, 0.8581964230189217, 6.0142781099283556e-15), (-6.014278109928377e-15, 0.8581964230189217, 6.0142781099283556e-15), (-6.014278109928377e-15, 0.8581964230189217, -6.0142781099283556e-15), (6.014278109928377e-15, 0.8581964230189217, -6.0142781099283556e-15), (0.8581964230189237, -0.8581964230189224, -0.8581964230189224), (-0.8581964230189237, -0.8581964230189224, -0.8581964230189224), (-6.014278109928377e-15, 0.8581964230189217, -6.0142781099283556e-15), (-6.014278109928377e-15, 0.8581964230189217, 6.0142781099283556e-15), (-0.8581964230189237, -0.8581964230189224, 0.8581964230189224), (-0.8581964230189237, -0.8581964230189224, -0.8581964230189224), (-0.8581964230189237, -0.8581964230189224, 0.8581964230189224), (0.8581964230189237, -0.8581964230189224, 0.8581964230189224), (6.014278109928377e-15, 0.8581964230189217, 6.0142781099283556e-15), (6.014278109928377e-15, 0.8581964230189217, -6.0142781099283556e-15), (0.8581964230189237, -0.8581964230189224, -0.8581964230189224), (0.8581964230189237, -0.8581964230189224, 0.8581964230189224)],
        'Rot1x': [(0.0, 1.0, 0.0), (5.58794e-09, 1.2739830000000001, 3.0513399999999998e-08), (0.0, 1.0, 0.0), (-0.382683, 0.92388000000000003, -1.3685799999999999e-07), (-0.48753200000000002, 1.1770069999999999, -7.7304800000000005e-08), (-0.382683, 0.92388000000000003, -1.3685799999999999e-07), (-0.70710700000000004, 0.70710700000000004, -2.52881e-07), (-0.90084200000000003, 0.90084200000000003, -3.01832e-07), (-0.70710700000000004, 0.70710700000000004, -2.52881e-07), (-0.92388000000000003, 0.382683, -3.3040500000000003e-07), (-1.1770069999999999, 0.48753200000000002, -4.108e-07), (-0.92388000000000003, 0.382683, -3.3040500000000003e-07), (-1.0, 0.0, -3.57628e-07), (-1.2739830000000001, 0.0, -4.4313799999999998e-07), (-1.0, 0.0, -3.57628e-07), (-0.92388000000000003, -0.382683, -3.3040500000000003e-07), (-1.1770069999999999, -0.48753200000000002, -4.0658100000000001e-07), (-0.92388000000000003, -0.382683, -3.3040500000000003e-07), (-0.70710700000000004, -0.70710700000000004, -2.52881e-07), (-0.90084200000000003, -0.90084200000000003, -3.4682100000000001e-07), (-0.70710700000000004, -0.70710700000000004, -2.52881e-07), (-0.382683, -0.92388000000000003, -1.3685799999999999e-07), (-0.48753200000000002, -1.1770069999999999, -1.2651399999999999e-07), (-0.382683, -0.92388000000000003, -1.3685799999999999e-07), (0.0, -1.0, 0.0), (5.58794e-09, -1.2739830000000001, 3.0513399999999998e-08), (0.0, -1.0, 0.0), (0.382683, -0.92388000000000003, 0.0), (0.48753200000000002, -1.1770069999999999, 2.1485700000000002e-08), (0.382683, -0.92388000000000003, 0.0), (0.70710700000000004, -0.70710700000000004, 0.0), (0.90084200000000003, -0.90084200000000003, 3.72543e-08), (0.70710700000000004, -0.70710700000000004, 0.0), (0.92388000000000003, -0.382683, 0.0), (1.1770069999999999, -0.48753200000000002, 3.0687899999999998e-08), (0.92388000000000003, -0.382683, 0.0), (1.0, 0.0, 0.0), (1.2739830000000001, 0.0, 4.1436799999999998e-08), (1.0, 0.0, 0.0), (0.92388000000000003, 0.382683, 0.0), (1.1770069999999999, 0.48753200000000002, 3.0687899999999998e-08), (0.92388000000000003, 0.382683, 0.0), (0.70710700000000004, 0.70710700000000004, 0.0), (0.90084200000000003, 0.90084200000000003, 3.6730800000000002e-08), (0.70710700000000004, 0.70710700000000004, 0.0), (0.382683, 0.92388000000000003, 0.0), (0.48753200000000002, 1.1770069999999999, 2.09622e-08), (0.382683, 0.92388000000000003, 0.0), (0.0, 1.0, 0.0), (-5.7024299999999998e-08, 0.92388000000000003, 0.382683), (-1.0536700000000001e-07, 0.70710700000000004, 0.70710700000000004), (-1.3766899999999999e-07, 0.382683, 0.92388000000000003), (-1.4901200000000001e-07, 0.0, 1.0), (-1.3766899999999999e-07, -0.382683, 0.92388000000000003), (-1.0536700000000001e-07, -0.70710700000000004, 0.70710700000000004), (-5.7024299999999998e-08, -0.92388000000000003, 0.382683), (0.0, -1.0, 0.0), (1.9388299999999999e-07, -0.92388000000000003, -0.382683), (3.5824800000000002e-07, -0.70710700000000004, -0.70710700000000004), (4.6807399999999999e-07, -0.382683, -0.92387900000000001), (5.0663899999999996e-07, 0.0, -1.0), (-0.382683, 0.0, -0.92388000000000003), (-0.70710600000000001, 0.0, -0.70710700000000004), (-0.92387900000000001, 0.0, -0.38268400000000002), (-1.0, 0.0, -3.57628e-07), (-0.92388000000000003, 0.0, 0.382683), (-0.70710700000000004, 0.0, 0.70710700000000004), (-0.38268400000000002, 0.0, 0.92387900000000001), (-1.4901200000000001e-07, 0.0, 1.0), (0.382683, 0.0, 0.92388000000000003), (0.70710700000000004, 0.0, 0.70710700000000004), (0.92388000000000003, 0.0, 0.382683), (1.0, 0.0, 0.0), (0.92388000000000003, 0.0, -0.382683), (0.70710700000000004, 0.0, -0.70710600000000001), (0.38268400000000002, 0.0, -0.92387900000000001), (5.0663899999999996e-07, 0.0, -1.0), (4.6807399999999999e-07, 0.382683, -0.92387900000000001), (3.5824800000000002e-07, 0.70710700000000004, -0.70710700000000004), (1.9388299999999999e-07, 0.92388000000000003, -0.382683), (0.0, 1.0, 0.0), (5.58794e-09, 1.2739830000000001, 3.0513399999999998e-08), (0.48753200000000002, 1.1770069999999999, 2.09622e-08), (0.90084200000000003, 0.90084200000000003, 3.6730800000000002e-08), (1.1770069999999999, 0.48753200000000002, 3.0687899999999998e-08), (1.2739830000000001, 0.0, 4.1436799999999998e-08), (1.1770069999999999, -0.48753200000000002, 3.0687899999999998e-08), (0.90084200000000003, -0.90084200000000003, 3.72543e-08), (0.48753200000000002, -1.1770069999999999, 2.1485700000000002e-08), (5.58794e-09, -1.2739830000000001, 3.0513399999999998e-08), (-0.48753200000000002, -1.1770069999999999, -1.2651399999999999e-07), (-0.90084200000000003, -0.90084200000000003, -3.4682100000000001e-07), (-1.1770069999999999, -0.48753200000000002, -4.0658100000000001e-07), (-1.2739830000000001, 0.0, -4.4313799999999998e-07), (-1.1770069999999999, 0.48753200000000002, -4.108e-07), (-0.90084200000000003, 0.90084200000000003, -3.01832e-07), (-0.48753200000000002, 1.1770069999999999, -7.7304800000000005e-08), (5.58794e-09, 1.2739830000000001, 3.0513399999999998e-08)],
        'Rot2x': [(5.58794e-09, 1.2739830000000001, 3.0513399999999998e-08), (0.0, 1.0, 0.0), (-5.7024299999999998e-08, 0.92388000000000003, 0.382683), (-7.6906199999999998e-08, 1.1770069999999999, 0.48753200000000002), (-5.7024299999999998e-08, 0.92388000000000003, 0.382683), (-1.0536700000000001e-07, 0.70710700000000004, 0.70710700000000004), (-1.49536e-07, 0.90084200000000003, 0.90084200000000003), (-1.0536700000000001e-07, 0.70710700000000004, 0.70710700000000004), (-1.3766899999999999e-07, 0.382683, 0.92388000000000003), (-1.93727e-07, 0.48753200000000002, 1.1770069999999999), (-1.3766899999999999e-07, 0.382683, 0.92388000000000003), (-1.4901200000000001e-07, 0.0, 1.0), (-1.89875e-07, -9.3132299999999997e-10, 1.2739830000000001), (-1.4901200000000001e-07, 0.0, 1.0), (-1.3766899999999999e-07, -0.382683, 0.92388000000000003), (-1.93727e-07, -0.48753200000000002, 1.1770069999999999), (-1.3766899999999999e-07, -0.382683, 0.92388000000000003), (-1.0536700000000001e-07, -0.70710700000000004, 0.70710700000000004), (-1.5058299999999999e-07, -0.90084200000000003, 0.90084200000000003), (-1.0536700000000001e-07, -0.70710700000000004, 0.70710700000000004), (-5.7024299999999998e-08, -0.92388000000000003, 0.382683), (-7.7953199999999994e-08, -1.1770069999999999, 0.48753200000000002), (-5.7024299999999998e-08, -0.92388000000000003, 0.382683), (0.0, -1.0, 0.0), (5.58794e-09, -1.2739830000000001, 3.0513399999999998e-08), (0.0, -1.0, 0.0), (1.9388299999999999e-07, -0.92388000000000003, -0.382683), (2.3935500000000001e-07, -1.1770069999999999, -0.48753200000000002), (1.9388299999999999e-07, -0.92388000000000003, -0.382683), (3.5824800000000002e-07, -0.70710700000000004, -0.70710700000000004), (4.41867e-07, -0.90084200000000003, -0.90084200000000003), (3.5824800000000002e-07, -0.70710700000000004, -0.70710700000000004), (4.6807399999999999e-07, -0.382683, -0.92387900000000001), (5.7807499999999997e-07, -0.48753200000000002, -1.1770069999999999), (4.6807399999999999e-07, -0.382683, -0.92387900000000001), (5.0663899999999996e-07, 0.0, -1.0), (6.4318599999999997e-07, 0.0, -1.2739830000000001), (5.0663899999999996e-07, 0.0, -1.0), (4.6807399999999999e-07, 0.382683, -0.92387900000000001), (5.7807499999999997e-07, 0.48753200000000002, -1.1770069999999999), (4.6807399999999999e-07, 0.382683, -0.92387900000000001), (3.5824800000000002e-07, 0.70710700000000004, -0.70710700000000004), (4.41867e-07, 0.90084200000000003, -0.90084200000000003), (3.5824800000000002e-07, 0.70710700000000004, -0.70710700000000004), (1.9388299999999999e-07, 0.92388000000000003, -0.382683), (2.3961499999999999e-07, 1.1770069999999999, -0.48753200000000002), (1.9388299999999999e-07, 0.92388000000000003, -0.382683), (0.0, 1.0, 0.0), (0.382683, 0.92388000000000003, 0.0), (0.48753200000000002, 1.1770069999999999, 2.09622e-08), (0.382683, 0.92388000000000003, 0.0), (0.70710700000000004, 0.70710700000000004, 0.0), (0.90084200000000003, 0.90084200000000003, 3.6730800000000002e-08), (0.70710700000000004, 0.70710700000000004, 0.0), (0.92388000000000003, 0.382683, 0.0), (1.1770069999999999, 0.48753200000000002, 3.0687899999999998e-08), (0.92388000000000003, 0.382683, 0.0), (1.0, 0.0, 0.0), (1.2739830000000001, 0.0, 4.1436799999999998e-08), (1.0, 0.0, 0.0), (0.92388000000000003, -0.382683, 0.0), (1.1770069999999999, -0.48753200000000002, 3.0687899999999998e-08), (0.92388000000000003, -0.382683, 0.0), (0.70710700000000004, -0.70710700000000004, 0.0), (0.90084200000000003, -0.90084200000000003, 3.72543e-08), (0.70710700000000004, -0.70710700000000004, 0.0), (0.382683, -0.92388000000000003, 0.0), (0.48753200000000002, -1.1770069999999999, 2.1485700000000002e-08), (0.382683, -0.92388000000000003, 0.0), (0.0, -1.0, 0.0), (5.58794e-09, -1.2739830000000001, 3.0513399999999998e-08), (0.0, -1.0, 0.0), (-0.382683, -0.92388000000000003, -1.3685799999999999e-07), (-0.48753200000000002, -1.1770069999999999, -1.2651399999999999e-07), (-0.382683, -0.92388000000000003, -1.3685799999999999e-07), (-0.70710700000000004, -0.70710700000000004, -2.52881e-07), (-0.90084200000000003, -0.90084200000000003, -3.4682100000000001e-07), (-0.70710700000000004, -0.70710700000000004, -2.52881e-07), (-0.92388000000000003, -0.382683, -3.3040500000000003e-07), (-1.1770069999999999, -0.48753200000000002, -4.0658100000000001e-07), (-0.92388000000000003, -0.382683, -3.3040500000000003e-07), (-1.0, 0.0, -3.57628e-07), (-1.2739830000000001, 0.0, -4.4313799999999998e-07), (-1.0, 0.0, -3.57628e-07), (-0.92387900000000001, 0.0, -0.38268400000000002), (-0.70710600000000001, 0.0, -0.70710700000000004), (-0.382683, 0.0, -0.92388000000000003), (5.0663899999999996e-07, 0.0, -1.0), (0.38268400000000002, 0.0, -0.92387900000000001), (0.70710700000000004, 0.0, -0.70710600000000001), (0.92388000000000003, 0.0, -0.382683), (1.0, 0.0, 0.0), (0.92388000000000003, 0.0, 0.382683), (0.70710700000000004, 0.0, 0.70710700000000004), (0.382683, 0.0, 0.92388000000000003), (-1.4901200000000001e-07, 0.0, 1.0), (-0.38268400000000002, 0.0, 0.92387900000000001), (-0.70710700000000004, 0.0, 0.70710700000000004), (-0.92388000000000003, 0.0, 0.382683), (-1.0, 0.0, -3.57628e-07), (-0.92388000000000003, 0.382683, -3.3040500000000003e-07), (-1.1770069999999999, 0.48753200000000002, -4.108e-07), (-0.92388000000000003, 0.382683, -3.3040500000000003e-07), (-0.70710700000000004, 0.70710700000000004, -2.52881e-07), (-0.90084200000000003, 0.90084200000000003, -3.01832e-07), (-0.70710700000000004, 0.70710700000000004, -2.52881e-07), (-0.382683, 0.92388000000000003, -1.3685799999999999e-07), (-0.48753200000000002, 1.1770069999999999, -7.7304800000000005e-08), (-0.382683, 0.92388000000000003, -1.3685799999999999e-07), (0.0, 1.0, 0.0), (5.58794e-09, 1.2739830000000001, 3.0513399999999998e-08), (2.3961499999999999e-07, 1.1770069999999999, -0.48753200000000002), (4.41867e-07, 0.90084200000000003, -0.90084200000000003), (5.7807499999999997e-07, 0.48753200000000002, -1.1770069999999999), (6.4318599999999997e-07, 0.0, -1.2739830000000001), (5.7807499999999997e-07, -0.48753200000000002, -1.1770069999999999), (4.41867e-07, -0.90084200000000003, -0.90084200000000003), (2.3935500000000001e-07, -1.1770069999999999, -0.48753200000000002), (5.58794e-09, -1.2739830000000001, 3.0513399999999998e-08), (-7.7953199999999994e-08, -1.1770069999999999, 0.48753200000000002), (-1.5058299999999999e-07, -0.90084200000000003, 0.90084200000000003), (-1.93727e-07, -0.48753200000000002, 1.1770069999999999), (-1.89875e-07, -9.3132299999999997e-10, 1.2739830000000001), (-1.93727e-07, 0.48753200000000002, 1.1770069999999999), (-1.49536e-07, 0.90084200000000003, 0.90084200000000003), (-7.6906199999999998e-08, 1.1770069999999999, 0.48753200000000002), (5.58794e-09, 1.2739830000000001, 3.0513399999999998e-08), (-0.48753200000000002, 1.1770069999999999, -7.7304800000000005e-08), (-0.90084200000000003, 0.90084200000000003, -3.01832e-07), (-1.1770069999999999, 0.48753200000000002, -4.108e-07), (-1.2739830000000001, 0.0, -4.4313799999999998e-07), (-1.1770069999999999, -0.48753200000000002, -4.0658100000000001e-07), (-0.90084200000000003, -0.90084200000000003, -3.4682100000000001e-07), (-0.48753200000000002, -1.1770069999999999, -1.2651399999999999e-07), (5.58794e-09, -1.2739830000000001, 3.0513399999999998e-08), (0.48753200000000002, -1.1770069999999999, 2.1485700000000002e-08), (0.90084200000000003, -0.90084200000000003, 3.72543e-08), (1.1770069999999999, -0.48753200000000002, 3.0687899999999998e-08), (1.2739830000000001, 0.0, 4.1436799999999998e-08), (1.1770069999999999, 0.48753200000000002, 3.0687899999999998e-08), (0.90084200000000003, 0.90084200000000003, 3.6730800000000002e-08), (0.48753200000000002, 1.1770069999999999, 2.09622e-08), (5.58794e-09, 1.2739830000000001, 3.0513399999999998e-08)],
        'Rot90': [(-0.0011259320493097603, 0.0, -1.235688320854984), (-0.0010363786695404176, 0.0, -1.0013653658063033), (0.30874527279542546, 0.0, -0.95230088857898842), (0.58820339051433501, 0.0, -0.80990990891533943), (0.80998269663713318, 0.0, -0.58813068118520362), (0.95237367630078174, 0.0, -0.30867256346629374), (1.0014376439757979, 0.0, 0.0011091123846692463), (1.253567413638929, 0.0, 0.0015730764776870727), (0.82097609149010409, 0.0, 0.40132812339552337), (0.41514998371483341, 0.0, 0.0015730764776870727), (0.66727967498530494, 0.0, 0.0011091123846692463), (0.63457037626740431, 0.0, -0.20541202540503767), (0.53964297809897677, 0.0, -0.39171739802131339), (0.39179014654677591, 0.0, -0.53957022957351453), (0.20548473473416975, 0.0, -0.63449754934928115), (-0.0010363786695404176, 0.0, -0.66720724003048637), (-0.0011259320493097603, 0.0, -0.43288420658914478), (-0.47370720508953768, 0.0, -0.81421484190889215), (-0.0011259320493097603, 0.0, -1.235688320854984)],
        'RoundBox': [(-1.193284629955085, 0.004221560000000111, -0.5), (1.193284629955085, 0.004221560000000111, -0.5), (1.6144211498672594, 0.004221560000000061, -0.2731012053364384), (1.853539, 0.004221559999999981, 0.08411617213403222), (1.853539, 0.004221559999999896, 0.4676500790271433), (1.847243001006953, 0.004221559999999891, 0.4907225620407551), (1.8250559520376803, 0.004221559999999889, 0.5), (-1.8250559520376803, 0.004221559999999889, 0.5), (-1.847243001006953, 0.004221559999999891, 0.4907225620407551), (-1.853539, 0.004221559999999896, 0.4676500790271433), (-1.853539, 0.004221559999999981, 0.08411617213403222), (-1.6144211498672594, 0.004221560000000061, -0.2731012053364384), (-1.193284629955085, 0.004221560000000111, -0.5)],
        'RoundRectangle': [(-3.607088, 0.008443120000000222, -1.0), (3.607088, 0.008443120000000222, -1.0), (3.677792, 0.008443120000000215, -0.970714), (3.707078, 0.0084431200000002, -0.90001), (3.707078, 0.0084431199999998, 0.90001), (3.677792, 0.008443119999999785, 0.970714), (3.607088, 0.008443119999999778, 1.0), (-3.607088, 0.008443119999999778, 1.0), (-3.677792, 0.008443119999999785, 0.970714), (-3.707078, 0.0084431199999998, 0.90001), (-3.707078, 0.0084431200000002, -0.90001), (-3.677792, 0.008443120000000215, -0.970714), (-3.607088, 0.008443120000000222, -1.0)],
        'Sphere': [(0.0, 1.0, 0.0), (-5.7024299999999998e-08, 0.92388000000000003, 0.382683), (-1.0536700000000001e-07, 0.70710700000000004, 0.70710700000000004), (-1.3766899999999999e-07, 0.382683, 0.92388000000000003), (-1.4901200000000001e-07, 0.0, 1.0), (-1.3766899999999999e-07, -0.382683, 0.92388000000000003), (-1.0536700000000001e-07, -0.70710700000000004, 0.70710700000000004), (-5.7024299999999998e-08, -0.92388000000000003, 0.382683), (0.0, -1.0, 0.0), (1.9388299999999999e-07, -0.92388000000000003, -0.382683), (3.5824800000000002e-07, -0.70710700000000004, -0.70710700000000004), (4.6807399999999999e-07, -0.382683, -0.92387900000000001), (5.0663899999999996e-07, 0.0, -1.0), (4.6807399999999999e-07, 0.382683, -0.92387900000000001), (3.5824800000000002e-07, 0.70710700000000004, -0.70710700000000004), (1.9388299999999999e-07, 0.92388000000000003, -0.382683), (0.0, 1.0, 0.0), (-0.382683, 0.92388000000000003, -1.3685799999999999e-07), (-0.70710700000000004, 0.70710700000000004, -2.52881e-07), (-0.92388000000000003, 0.382683, -3.3040500000000003e-07), (-1.0, 0.0, -3.57628e-07), (-0.92388000000000003, -0.382683, -3.3040500000000003e-07), (-0.70710700000000004, -0.70710700000000004, -2.52881e-07), (-0.382683, -0.92388000000000003, -1.3685799999999999e-07), (0.0, -1.0, 0.0), (0.382683, -0.92388000000000003, 0.0), (0.70710700000000004, -0.70710700000000004, 0.0), (0.92388000000000003, -0.382683, 0.0), (1.0, 0.0, 0.0), (0.92388000000000003, 0.0, -0.382683), (0.70710700000000004, 0.0, -0.70710600000000001), (0.38268400000000002, 0.0, -0.92387900000000001), (5.0663899999999996e-07, 0.0, -1.0), (-0.382683, 0.0, -0.92388000000000003), (-0.70710600000000001, 0.0, -0.70710700000000004), (-0.92387900000000001, 0.0, -0.38268400000000002), (-1.0, 0.0, -3.57628e-07), (-0.92388000000000003, 0.0, 0.382683), (-0.70710700000000004, 0.0, 0.70710700000000004), (-0.38268400000000002, 0.0, 0.92387900000000001), (-1.4901200000000001e-07, 0.0, 1.0), (0.382683, 0.0, 0.92388000000000003), (0.70710700000000004, 0.0, 0.70710700000000004), (0.92388000000000003, 0.0, 0.382683), (1.0, 0.0, 0.0), (0.92388000000000003, 0.382683, 0.0), (0.70710700000000004, 0.70710700000000004, 0.0), (0.382683, 0.92388000000000003, 0.0), (0.0, 1.0, 0.0)],
        'Spine': [(-1.1102230246251565e-16, 5e-13, -2.032667320992919), (0.49354449026686203, -5.004801928132631e-13, -1.4806339414812908), (0.8225684212058854, -5.154834162246023e-13, -1.4753682332757023), (1.1300110524486182, -5.266512067055175e-13, -1.3598335148040293), (1.3598327656717721, -5.266512067055175e-13, -1.13001186349979), (1.4753678365443892, -5.154834162246023e-13, -0.8225689694459764), (1.4806334708005862, 5.004801928132631e-13, -0.49354449026686203), (2.032538775635916, -5e-13, -1.1102230246251565e-16), (1.482813638259908, -4.935444902668618e-13, 0.4870637454726206), (1.4753672305092795, -5.154834162246023e-13, 0.8225805459381262), (1.3598322084015348, -5.266512067055175e-13, 1.130029966493216), (1.130010680935127, -5.266512067055175e-13, 1.3598514939596251), (0.8225682393953531, -5.154834162246023e-13, 1.4753795673538086), (0.4935444608493181, -5.004801928132631e-13, 1.4806335884707624), (1.1102230246251565e-16, -5e-13, 2.0308844518050986), (-0.49123907656936144, -4.672055440305117e-30, 1.4811806714861477), (-0.8225811244799714, -5.154834162246023e-13, 1.4753796279573197), (-1.130030839463304, -5.266512067055175e-13, 1.3598515558785396), (-1.3598524288486264, -5.266512067055175e-13, 1.1300300284121316), (-1.4753802671026761, -5.154834162246023e-13, 0.8225806065416371), (-1.4806337061409385, 5.004801928132631e-13, 0.4935444902668621), (-2.0305988401906236, 5e-13, 1.1102230246251565e-16), (-1.480898749358913, 4.935444902668618e-13, -0.4870637454726206), (-1.4753802671026761, -5.154834162246023e-13, -0.8225688482389539), (-1.3598524907675418, -5.266512067055175e-13, -1.1300116777430445), (-1.1300309633011334, -5.266512067055175e-13, -1.3598333290472826), (-0.8225812153852384, -5.154834162246023e-13, -1.475368172672192), (-0.493544490266862, -5.004801928132631e-13, -1.4806339414812908), (-1.1102230246251565e-16, 5e-13, -2.032667320992919)],
        'Square': [(-1.0000000000000009, 0.0, -1.0000000000000002), (-1.0000000000000009, 0.0, 1.0000000000000002), (1.0000000000000009, 0.0, 1.0000000000000002), (1.0000000000000009, 0.0, -1.0000000000000002), (-1.0000000000000009, 0.0, -1.0000000000000002)],
        'Square45': [(0.0, 0.0, -1.0000000000000002), (-1.0000000000000009, 0.0, 0.0), (0.0, 0.0, 1.0000000000000002), (1.0000000000000009, 0.0, 0.0), (0.0, 0.0, -1.0000000000000002)],
        'Star': [(-0.98692860584775277, 0.0, 0.0), (-0.12908402837950977, 0.0, 0.054363063134164273), (-0.052819097502875938, 0.0, 0.13052975276952827), (0.0012481713826372883, 0.0, 0.98817677723039032), (0.055315357920085698, 0.0, 0.130529835117593), (0.1317780065002303, 0.0, 0.054067186537448415), (0.98942494861302777, 0.0, 0.0), (0.1317780065002303, 0.0, -0.054067186537448415), (0.055315440268150461, 0.0, -0.13052975276952827), (0.0012481713826372883, 0.0, -0.98817677723039032), (-0.052819015154811209, 0.0, -0.130529835117593), (-0.13106655803882827, 0.0, -0.053955275517427022), (-0.98692860584775277, 0.0, 0.0), (-0.1292816637349557, 0.054067186537448415, 0.0), (-0.052819015154811209, 0.130529835117593, 0.0), (0.0012481713826372883, 0.99202605516996323, 0.0), (0.055315357920085698, 0.130529835117593, 0.0), (0.1381488645310994, 0.053667304334929132, 0.0), (0.98942494861302777, 0.0, 0.0), (0.1317780065002303, -0.054067186537448415, 0.0), (0.055315357920085698, -0.130529835117593, 0.0), (0.0012481713826372883, -0.99202605516996323, 0.0), (-0.052819015154811209, -0.130529835117593, 0.0), (-0.1292816637349557, -0.054067186537448415, 0.0), (-0.99077788378732712, 0.0, 0.0), (-0.1292816637349557, -0.054067186537448415, 0.0), (-0.052819015154811209, -0.130529835117593, 0.0), (0.0012481713826372883, -0.99202605516996323, 0.0), (0.0012481713826372883, -0.13168320211275031, 0.053994802588516216), (0.0012481713826372883, -0.055404519109300235, 0.1296362762667824), (0.0012481713826372883, 0.0, 0.99202605516996323), (0.0012481713826372883, 0.053877703640414498, 0.13354879752009671), (0.0012481713826372883, 0.130529835117593, 0.054067186537448415), (0.0012481713826372883, 0.99202605516996323, 0.0), (0.0012481713826372883, 0.130529835117593, -0.054067186537448415), (0.0012481713826372883, 0.054067186537448415, -0.13052975276952827), (0.0012481713826372883, 0.0, -0.99202605516996323), (0.0012481713826372883, -0.054067186537448415, -0.13052975276952827), (0.0012481713826372883, -0.130529835117593, -0.054067186537448415), (0.0012481713826372883, -0.99202605516996323, 0.0)],
        'Triangle': [(0.012637538835406303, 1.8540903532977005e-17, -0.30279590934515), (0.3033009469509125, -1.2286139280849109e-17, 0.20064788311719894), (0.29066339135169983, -1.3626446497148382e-17, 0.22253675013780594), (-0.29066339135169983, -1.3626446497148382e-17, 0.22253675013780594), (-0.3033009469509125, -1.2286139280849109e-17, 0.20064788311719894), (-0.012637538835406303, 1.8540903532977005e-17, -0.30279590934515), (0.012637538835406303, 1.8540903532977005e-17, -0.30279590934515)],
        'Gimbal': [(0.06173487239404082, -1.9603099179757053e-11, -14.25534046923868), (1.4982115278650978, -1.9603099179757053e-11, -13.87043766480293), (2.549785378900415, -1.9603099179757053e-11, -12.818863813767619), (2.934688183336155, -1.9603099179757053e-11, -11.382387158296558), (8.23058904165564, -2.2468578627642374e-11, -11.379252793388721), (10.215818421773786, -2.2468578627642374e-11, -10.233080153149258), (11.361991062013255, -2.2468578627642374e-11, -8.247850773031109), (11.370687939961057, -1.9603099179757053e-11, -2.9519499147116215), (12.807164595432118, -1.9603099179757053e-11, -2.5670471102758814), (13.858738446467429, -1.9603099179757053e-11, -1.5154732592405642), (14.243641250903176, -1.9603099179757053e-11, -0.07899660376950719), (13.858738446467429, -1.9603099179757053e-11, 1.3574800517015497), (12.807164595432118, -1.9603099179757053e-11, 2.4090539027368667), (11.370687939961057, -1.9603099179757053e-11, 2.793956707172607), (11.361991062013253, -2.2468578627642374e-11, 8.089857565492087), (10.215818421773786, -2.2468578627642374e-11, 10.075086945610236), (8.230589041655637, -2.2468578627642374e-11, 11.221259585849703), (2.934688183336155, -1.9603099179757053e-11, 11.201164244459006), (2.549785378900415, -1.9603099179757053e-11, 12.637640899930082), (1.4982115278650978, -1.9603099179757053e-11, 13.689214750965398), (0.06173487239404082, -1.9603099179757053e-11, 14.074117555401141), (-1.374741783077016, -1.9603099179757053e-11, 13.689214750965398), (-2.426315634112333, -1.9603099179757053e-11, 12.637640899930082), (-2.811218438548073, -1.9603099179757053e-11, 11.201164244459006), (-8.107119296867559, -2.2468578627642374e-11, 11.221259585849708), (-10.092348676985706, -2.2468578627642374e-11, 10.075086945610243), (-11.238521317225175, -2.2468578627642374e-11, 8.089857565492094), (-11.247218195172982, -1.9603099179757053e-11, 2.793956707172607), (-12.683694850644045, -1.9603099179757053e-11, 2.4090539027368667), (-13.735268701679356, -1.9603099179757053e-11, 1.3574800517015497), (-14.120171506115097, -1.9603099179757053e-11, -0.07899660376950719), (-13.735268701679356, -1.9603099179757053e-11, -1.5154732592405642), (-12.683694850644045, -1.9603099179757053e-11, -2.5670471102758814), (-11.247218195172982, -1.9603099179757053e-11, -2.9519499147116215), (-11.238521317225173, -2.2468578627642374e-11, -8.247850773031102), (-10.092348676985706, -2.2468578627642374e-11, -10.23308015314925), (-8.107119296867557, -2.2468578627642374e-11, -11.379252793388716), (-2.811218438548073, -1.9603099179757053e-11, -11.382387158296558), (-2.426315634112333, -1.9603099179757053e-11, -12.818863813767619), (-1.374741783077016, -1.9603099179757053e-11, -13.87043766480293), (0.06173487239404082, -1.9603099179757053e-11, -14.25534046923868)],
        'Main': [(0.04818331503924993, -2.0219387901033767e-11, -15.46812164604257), (1.9817252336966233, -2.0219387901033767e-11, -14.950030590298109), (3.3971760966095306, -2.0219387901033767e-11, -13.534579727385204), (3.915267152353996, -2.0219387901033767e-11, -12.068819635853497), (12.04818331503926, -2.0219387901033767e-11, -12.066188511488111), (12.039604169693693, -2.0219387901033767e-11, -3.9444826185138173), (13.40933188023138, -2.0219387901033767e-11, -3.426391562769353), (14.82478274314426, -2.0219387901033767e-11, -2.0109406998564436), (15.342873798888736, -2.0219387901033767e-11, -0.07739878119907063), (14.82478274314426, -2.0219387901033767e-11, 1.8561431374583022), (13.40933188023138, -2.0219387901033767e-11, 3.2715940003712096), (12.039604169693693, -2.0219387901033767e-11, 3.789685056115675), (12.04818331503926, -2.0219387901033767e-11, 11.933811488511896), (3.915267152353996, -2.0219387901033767e-11, 11.958012918447153), (3.3971760966095306, -2.0219387901033767e-11, 13.327740628984852), (1.9817252336966233, -2.0219387901033767e-11, 14.743191491897763), (0.04818331503924993, -2.0219387901033767e-11, 15.261282547642235), (-1.8853586036181222, -2.0219387901033767e-11, 14.743191491897763), (-3.3008094665310295, -2.0219387901033767e-11, 13.327740628984852), (-3.818900522275495, -2.0219387901033767e-11, 11.958012918447153), (-11.951816684960761, -2.0219387901033767e-11, 11.933811488511896), (-11.943237539615192, -2.0219387901033767e-11, 3.789685056115675), (-13.258759020733718, -2.0219387901033767e-11, 3.2715940003712096), (-14.674209883646602, -2.0219387901033767e-11, 1.8561431374583022), (-15.19230093939108, -2.0219387901033767e-11, -0.07739878119907063), (-14.674209883646602, -2.0219387901033767e-11, -2.0109406998564436), (-13.258759020733718, -2.0219387901033767e-11, -3.426391562769353), (-11.943237539615192, -2.0219387901033767e-11, -3.9444826185138173), (-11.951816684960761, -2.0219387901033767e-11, -12.066188511488111), (-3.818900522275495, -2.0219387901033767e-11, -12.068819635853497), (-3.3008094665310295, -2.0219387901033767e-11, -13.534579727385204), (-1.8853586036181222, -2.0219387901033767e-11, -14.950030590298109), (0.04818331503924993, -2.0219387901033767e-11, -15.46812164604257)],
        'Offset': [(-9.069795324897036, -2.1298758235487955e-11, -9.216334227635214), (-7.2844858043538805, -2.1298758235487955e-11, -10.24708309086422), (-5.222988077895868, -2.1298758235487955e-11, -10.247083090864221), (5.3464578226839485, -2.1298758235487955e-11, -10.247083090864225), (7.407955549141961, -2.1298758235487955e-11, -10.247083090864226), (9.193265069685118, -2.1298758235487955e-11, -9.216334227635219), (10.224013932914124, -2.1298758235487955e-11, -7.431024707092061), (10.224013932914124, -2.1298758235487955e-11, -5.369526980634049), (10.224013932914122, -2.1298758235487955e-11, 5.199918919945767), (10.224013932914122, -2.1298758235487955e-11, 7.261416646403779), (9.193265069685115, -2.1298758235487955e-11, 9.046726166946938), (7.407955549141959, -2.1298758235487955e-11, 10.077475030175943), (-7.284485804353882, -2.1298758235487955e-11, 10.07747503017595), (-9.06979532489704, -2.1298758235487955e-11, 9.046726166946943), (-10.100544188126046, -2.1298758235487955e-11, 7.261416646403785), (-10.100544188126044, -2.1298758235487955e-11, -7.431024707092055), (-9.069795324897036, -2.1298758235487955e-11, -9.216334227635214)],
        'WrapArrow': [(-0.30901699999999999, 0.0, -0.95105700000000004), (-0.80901699999999999, -0.41799999999999998, -0.587785), (-0.80901699999999999, -0.20899999999999999, -0.587785), (-0.95105700000000004, -0.20899999999999999, -0.30901699999999999), (-1.0, -0.20899999999999999, 0.0), (-0.95105700000000004, -0.20899999999999999, 0.30901699999999999), (-0.80901699999999999, -0.20899999999999999, 0.587785), (-0.587785, -0.20899999999999999, 0.80901699999999999), (-0.30901699999999999, -0.20899999999999999, 0.95105700000000004), (-2.9802300000000001e-08, -0.20899999999999999, 1.0), (0.30901699999999999, -0.20899999999999999, 0.95105700000000004), (0.587785, -0.20899999999999999, 0.80901699999999999), (0.80901699999999999, -0.20899999999999999, 0.587785), (0.95105700000000004, -0.20899999999999999, 0.30901699999999999), (1.0, -0.20899999999999999, 0.0), (0.95105700000000004, -0.20899999999999999, -0.30901699999999999), (0.80901800000000001, -0.20899999999999999, -0.58778600000000003), (0.80901800000000001, -0.41799999999999998, -0.58778600000000003), (0.30901699999999999, 0.0, -0.95105700000000004), (0.80901800000000001, 0.41799999999999998, -0.58778600000000003), (0.80901800000000001, 0.20899999999999999, -0.58778600000000003), (0.95105700000000004, 0.20899999999999999, -0.30901699999999999), (1.0, 0.20899999999999999, 0.0), (0.95105700000000004, 0.20899999999999999, 0.30901699999999999), (0.80901699999999999, 0.20899999999999999, 0.587785), (0.587785, 0.20899999999999999, 0.80901699999999999), (0.30901699999999999, 0.20899999999999999, 0.95105700000000004), (-2.9802300000000001e-08, 0.20899999999999999, 1.0), (-0.30901699999999999, 0.20899999999999999, 0.95105700000000004), (-0.587785, 0.20899999999999999, 0.80901699999999999), (-0.80901699999999999, 0.20899999999999999, 0.587785), (-0.95105700000000004, 0.20899999999999999, 0.30901699999999999), (-1.0, 0.20899999999999999, 0.0), (-0.95105700000000004, 0.20899999999999999, -0.30901699999999999), (-0.80901699999999999, 0.20899999999999999, -0.587785), (-0.80901699999999999, 0.41799999999999998, -0.587785), (-0.30901699999999999, 0.0, -0.95105700000000004)],
        'XYZArrow': [(0.0006904514387826577, 1.9894576277842517, -9.793130305446596e-05), (0.0006904514387826577, 1.7301137621562863, 0.23204279088845325), (0.0006904514387826577, 1.728911283861412, 0.08455581750289065), (0.0006904514387826577, 0.08402064586988278, 0.08455581750289065), (0.0006904514387826577, 0.08402064586988278, 1.729182032891202), (0.0006904514387826577, 0.2093457980781212, 1.731361711484285), (0.0006904514387826577, -0.00036868033284853663, 1.9886386161211362), (0.20547459591277661, -0.00036868033284853663, 1.731361711484285), (0.07456559920777783, -0.00036868033284853663, 1.729182032891202), (0.07456559920777783, -0.00036868033284853663, 0.07932223077067202), (1.7299704156330407, -0.00036868033284853663, 0.07932223077067202), (1.7357600444268373, -0.00036868033284853663, 0.22249245013622998), (1.9862537704171483, -0.00036868033284853663, -9.793130305446596e-05), (1.7357600444268373, 0.20325338811085036, -9.793130305446596e-05), (1.7299704156330407, 0.08724653874618932, -9.793130305446596e-05), (0.07215608360762427, 0.08724653874618932, -9.793130305446596e-05), (0.07215608360762427, 1.728911283861412, -9.793130305446596e-05), (0.22675715691187648, 1.7301137621562863, -9.793130305446596e-05), (0.0006904514387826577, 1.9894576277842517, -9.793130305446596e-05), (0.0012615885409990035, 0.0002024567693678092, 0.0004732057991618799), (1.9862537704171483, -0.00036868033284853663, -9.793130305446596e-05), (0.0012615885409990035, 0.0002024567693678092, 0.0004732057991618799), (0.0006904514387826577, -0.00036868033284853663, 1.9886386161211362)],
    }

    if shape in ctrls_shapes_dict:
        return ctrls_shapes_dict[shape]

def create_ctrl(name, side, index, size, color, shape, cvs_pos, axis,
                lock_attrs, parent, animation_set, translation=(0, 0, 0), rotation=(0, 0, 0), scale=(1, 1, 1)):

    """

    :param name:         (str) Controller name.
    :param side:         (str/None) Controller side.
    :param index:        (int/None) Controller index.
    :param size:         (int) Controller size.
    :param color:        (int) Controller color.
    :param shape:        (str) Controller shape.
    :param cvs_pos:      (list/None) Controller shape cvs positions list.
    :param axis:         (str) Controller orientation. 'X+'/'X-'/'Y+'/'Y-'/'Z+'/'Z-'.
    :param lock_attrs:   (list) Controller attributes to lock.
    :param parent:       (str/None) Controller's upper level parent.
    :param animation_set:(str/None) Animation controllers set.
    :param translation:  (tuple) Controller's world rotation position.
    :param rotation:     (tuple) Controller's world rotation position.
    :param scale:        (tuple) Controller's scale position.
    :return:             (str) Controller full name
    """

    # get index
    if side and side.lower() != 'none':
        side = '{}_'.format(side)
    else:
        side = ''

    # get index
    if type(index) == int:
        index = '_{:03d}'.format(index)
    else:
        index = ''

    ctrl_name = 'ctrl_{}{}{}'.format(side, name, index)

    if cmds.objExists(ctrl_name):
        raise ValueError('{} already exists in the scene'.format(ctrl_name))

    if shape == 'Custom':
        pos_list = cvs_pos
    else:
        pos_list = get_ctrl_shape_pos(shape=shape)

    if pos_list:
        ctrl = cmds.curve(name=ctrl_name, p=pos_list, d=1)
        cmds.rename(cmds.listRelatives(ctrl, shapes=True)[0], '{}Shape'.format(ctrl))

        # set size, color, axis
        cmds.setAttr('{}.s'.format(ctrl), size, size, size)
        if axis == "X+":
            cmds.setAttr("{}.rx".format(ctrl), 90)
        elif axis == "X-":
            cmds.setAttr("{}.rx".format(ctrl), -90)
        elif axis == "Y+":
            cmds.setAttr("{}.ry".format(ctrl), 90)
        elif axis == "Y-":
            cmds.setAttr("{}.ry".format(ctrl), -90)
        elif axis == "Z+":
            cmds.setAttr("{}.rz".format(ctrl), 90)
        elif axis == "Z-":
            cmds.setAttr("{}.rz".format(ctrl), -90)

        cmds.makeIdentity(ctrl_name, apply=True, t=True, r=True, s=True, n=False, pn=True)

        # set color
        cmds.setAttr("{}Shape.overrideEnabled".format(ctrl), True)
        cmds.setAttr("{}Shape.overrideColor".format(ctrl), color)

    else:
        ctrl = cmds.group(name=ctrl_name, empty=True)

    # add group above
    offset_grp =  add_extra_group(obj=ctrl, grp_name=ctrl.replace("ctrl", "grpOffset"),
                                  world_orient=False)
    zero_grp = add_extra_group(obj=offset_grp, grp_name=offset_grp.replace("grpOffset", "zero"),
                               world_orient=False)
    attrUtils.lock_and_hide_attrs(obj=ctrl, attrs_list=["rotateOrder"], lock=False, hide=False)
    attrUtils.lock_and_hide_attrs(obj=ctrl, attrs_list=lock_attrs, lock=True, hide=True)
    for obj in [ctrl, offset_grp, zero_grp]:
        attrUtils.lock_and_hide_attrs(obj=obj, attrs_list=["v"], lock=True, hide=True)

    # set position
    cmds.xform(zero_grp, scale=scale)
    cmds.xform(zero_grp, ws=True, a=True, translation=translation, rotation=rotation)

    # parent to parent
    if parent:
        cmds.parent(zero_grp, parent)

    # parent to set
    if animation_set:
        animation_ctrls_set = animation_set
        if not cmds.objExists(animation_set) or cmds.nodeType(animation_set) != "objectSet":
            animation_ctrls_set = cmds.sets(name=animation_set, empty=True)

        cmds.sets(ctrl, edit=True, forceElement=animation_ctrls_set)
        cmds.sets(offset_grp, edit=True, forceElement=animation_ctrls_set)

    return ctrl


def add_extra_group(obj, grp_name, world_orient=False):
    """

    :param obj: str\\Maya object to add an extra group.
    :param grp_name: str\\The extra group name.
    :param world_orient: bool\\Set the new group world oriented or not.
    :return: str\\The newly added group.
    """

    obj_grp = cmds.group(name=grp_name, empty=True)
    t_pos = cmds.xform(obj, q=True, ws=True, t=True)
    r_pos = cmds.xform(obj, q=True, ws=True, ro=True)
    if world_orient:
        r_pos = [0, 0, 0]
    s_pos = cmds.xform(obj, q=True, ws=True, s=True)

    cmds.xform(obj_grp, s=s_pos)
    cmds.xform(obj_grp, ws=True, t=t_pos)
    cmds.xform(obj_grp, ws=True, ro=r_pos)

    obj_parent = cmds.listRelatives(obj, parent=True)
    if obj_parent:
        cmds.parent(obj_grp, obj_parent[0], absolute=True)
        cmds.parent(obj, obj_grp, absolute=True)
    else:
        cmds.parent(obj, obj_grp, absolute=True)

    return obj_grp


def solve_double_transform(ctrl, inverse_grp_name):
    """Add inverse transform group on controller to avoid double transform.
    usually used on facial or secondary controllers.

    :param ctrl: (str): Controller to add on inverse transform group layer.
    :param inverse_grp_name: (str): Inverse transform group name.

    """

    inverse_grp = inverse_grp_name
    if not cmds.objExists(inverse_grp):
        inverse_grp = add_extra_group(obj=ctrl, grp_name=inverse_grp_name, world_orient=False)

    inverse_mdn = cmds.createNode("multiplyDivide", name="{}_inverseMDN".format(ctrl))
    cmds.setAttr("{}.operation".format(inverse_mdn), 1)

    for attr in ["X", "Y", "Z"]:
        # delete existing connection on inverse offset group's translate attributes
        inverse_grp_connection = cmds.listConnections(
            "{}.translate{}".format(inverse_grp, attr), plugs=True, source=True
        )
        if inverse_grp_connection:
            cmds.disconnectAttr(inverse_grp_connection[0], "{}.translate{}".format(inverse_grp, attr))

        # connect inverse transform relation
        cmds.setAttr("{}.input2{}".format(inverse_mdn, attr), -1)
        cmds.connectAttr("{}.translate{}".format(ctrl, attr), "{}.input1{}".format(inverse_mdn, attr))
        cmds.connectAttr("{}.output{}".format(inverse_mdn, attr), "{}.translate{}".format(inverse_grp, attr))
# ============================ 通用输入对话框 ============================
"""Maya GUI 辅助：通用输入对话框，供工具脚本收集参数。"""

try:
    from PySide2 import QtWidgets, QtCore
except ImportError:
    from PySide import QtGui as QtWidgets
    QtCore = QtWidgets.QtCore


def prompt_fields(title, fields):
    """弹出一个多字段输入对话框。

    :param title: (str) 对话框标题。
    :param fields: (list) [(标签, 默认值), ...]。
    :return: (list/None) 各字段字符串值列表；取消时返回 None。
    """
    dialog = QtWidgets.QDialog()
    dialog.setWindowTitle(title)
    dialog.setMinimumWidth(320)

    layout = QtWidgets.QVBoxLayout(dialog)
    form = QtWidgets.QFormLayout()
    edits = []
    for label, default in fields:
        edit = QtWidgets.QLineEdit(str(default) if default is not None else '')
        edits.append(edit)
        form.addRow(label, edit)
    layout.addLayout(form)

    buttons = QtWidgets.QDialogButtonBox(
        QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
    buttons.accepted.connect(dialog.accept)
    buttons.rejected.connect(dialog.reject)
    layout.addWidget(buttons)

    if dialog.exec_() == QtWidgets.QDialog.Accepted:
        return [e.text() for e in edits]
    return None
# ============================ 工具脚本 ============================
CTRL_COLORS = {'m': 17,
               'l': 6,
               'r': 13}

SUB_COLORS = {'m': 25,
              'l': 15,
              'r': 4}


def run_ctrl_standard_hierarchy():
    """给选中的控制器生成标准层级 zero/driven/connect/offset，
    并复制一个子控制器，创建 output 组连接子控制器属性。"""
    ctrls = cmds.ls(selection=True)
    if not ctrls:
        cmds.warning("请先选择控制器曲线。")
        return

    for ctrl in ctrls:
        # get name parts
        name_parts = ctrl.split('_')

        # create zero group
        zero = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'zero_'))
        # create driven group
        driven = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'driven_'), parent=zero)
        # create connect group
        connect = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'connect_'), parent=driven)
        # create offset group
        offset = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'offset_'), parent=connect)

        # snap to control position
        cmds.matchTransform(zero, ctrl, position=True, rotation=True)

        # parent control to offset group
        cmds.parent(ctrl, offset)

        # freeze transformation for controller
        cmds.makeIdentity(ctrl, apply=True, scale=True)
        # delete history
        cmds.select(ctrl)
        cmds.DeleteHistory()

        # duplicate ctrl as sub control
        sub = cmds.duplicate(ctrl, name=ctrl.replace(name_parts[2], name_parts[2] + 'Sub'))[0]
        cmds.parent(sub, ctrl)
        cmds.setAttr(sub + '.scale', 0.9, 0.9, 0.9)
        cmds.makeIdentity(sub, apply=True, scale=True)

        # create output group
        output = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'output_'), parent=ctrl)

        # connect attrs
        cmds.connectAttr(sub + '.translate', output + '.translate')
        cmds.connectAttr(sub + '.rotate', output + '.rotate')
        cmds.connectAttr(sub + '.rotateOrder', output + '.rotateOrder')

        # show rotate order
        cmds.setAttr(ctrl + '.rotateOrder', channelBox=True)
        cmds.setAttr(sub + '.rotateOrder', channelBox=True)

        # add sub vis attr
        cmds.addAttr(ctrl, longName='subCtrlVis', attributeType='bool')
        cmds.setAttr(ctrl + '.subCtrlVis', channelBox=True)

        # connect sub vis
        cmds.connectAttr(ctrl + '.subCtrlVis', sub + '.visibility')

        # lock hide unused attrs
        for attr in ['scaleX', 'scaleY', 'scaleZ', 'visibility']:
            for ctrl_node in [ctrl, sub]:
                cmds.setAttr('{}.{}'.format(ctrl_node, attr), keyable=False, channelBox=False, lock=True)

        # set color
        for ctrl_node, col_idx in zip([ctrl, sub], [CTRL_COLORS[name_parts[1]], SUB_COLORS[name_parts[1]]]):
            # get shape node
            shape_node = cmds.listRelatives(ctrl_node, shapes=True)[0]
            # set color
            cmds.setAttr(shape_node + '.overrideEnabled', 1)
            cmds.setAttr(shape_node + '.overrideColor', col_idx)
CTRL_COLORS = {'m': 17,
               'l': 6,
               'r': 13}

SUB_COLORS = {'m': 25,
              'l': 15,
              'r': 4}


def run_ctrl_hierarchy_auto_parent():
    """给选中的控制器生成标准层级，并按编号自动父子（编号大的 zero 组 p 到编号小一层的控制器下）。"""
    ctrls = cmds.ls(selection=True)
    if not ctrls:
        cmds.warning("请先选择控制器曲线。")
        return

    zeros_ctrls = []

    for ctrl in ctrls:
        # get name parts
        name_parts = ctrl.split('_')

        # create zero group
        zero = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'zero_'))
        zeros_ctrls.append(zero)
        zeros_ctrls.append(ctrl)
        # create driven group
        driven = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'driven_'), parent=zero)
        # create connect group
        connect = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'connect_'), parent=driven)
        # create offset group
        offset = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'offset_'), parent=connect)

        # snap to control position
        cmds.matchTransform(zero, ctrl, position=True, rotation=True)

        # parent control to offset group
        cmds.parent(ctrl, offset)

        # freeze transformation for controller
        cmds.makeIdentity(ctrl, apply=True, scale=True)
        # delete history
        cmds.select(ctrl)
        cmds.DeleteHistory()

        # show rotate order
        cmds.setAttr(ctrl + '.rotateOrder', channelBox=True)

        # lock hide unused attrs
        for attr in ['visibility']:
            for ctrl_node in [ctrl]:
                cmds.setAttr('{}.{}'.format(ctrl_node, attr), keyable=False, channelBox=False, lock=True)

        # set color
        for ctrl_node, col_idx in zip([ctrl], [CTRL_COLORS[name_parts[1]]]):
            # get shape node
            shape_node = cmds.listRelatives(ctrl_node, shapes=True)[0]
            # set color
            cmds.setAttr(shape_node + '.overrideEnabled', 1)
            cmds.setAttr(shape_node + '.overrideColor', col_idx)

    dict_zeros_ctrls = {}

    index_max = len(ctrls)
    for tra in zeros_ctrls:
        parts = tra.split('_')
        key = int(parts[3])

        if key not in dict_zeros_ctrls:
            dict_zeros_ctrls[key] = []
        dict_zeros_ctrls[key].append(tra)

    for i in range(index_max, 1, -1):
        zero_name = dict_zeros_ctrls[i][0]
        ctrl_name = dict_zeros_ctrls[i - 1][1]

        if cmds.objExists(zero_name) and cmds.objExists(ctrl_name):
            cmds.parent(zero_name, ctrl_name)
zeros = []
ctrls_a = []


def _ctrl_hiracchy(ctrls):
    CTRL_COLORS = {'m': 17,
                   'l': 6,
                   'r': 13}

    SUB_COLORS = {'m': 25,
                  'l': 15,
                  'r': 4}

    for ctrl in ctrls:
        name_parts = ctrl.split('_')

        zero = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'zero_'))
        driven = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'driven_'), parent=zero)
        connect = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'connect_'), parent=driven)
        offset = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'offset_'), parent=connect)

        zeros.append(zero)
        ctrls_a.append(ctrl)

        cmds.matchTransform(zero, ctrl, position=True, rotation=True)

        cmds.parent(ctrl, offset)

        cmds.makeIdentity(ctrl, apply=True, scale=True)
        cmds.select(ctrl)
        cmds.DeleteHistory()

        cmds.setAttr(ctrl + '.rotateOrder', channelBox=True)

        for attr in ['visibility']:
            for ctrl_node in [ctrl]:
                cmds.setAttr('{}.{}'.format(ctrl_node, attr), keyable=False, channelBox=False, lock=True)

        for ctrl_node, col_idx in zip([ctrl], [CTRL_COLORS[name_parts[1]]]):
            shape_node = cmds.listRelatives(ctrl_node, shapes=True)[0]
            cmds.setAttr(shape_node + '.overrideEnabled', 1)
            cmds.setAttr(shape_node + '.overrideColor', col_idx)


def run_bind_ctrls_to_joint_chain():
    """选中骨骼链最上层关节，加选用作控制器形状的曲线，
    为每根骨骼复制一个控制器并建立层级，最后用控制器约束对应骨骼。"""
    sel = cmds.ls(selection=True)
    if len(sel) < 2:
        cmds.warning("请先选择骨骼链最上层关节，再加选控制器曲线。")
        return

    zeros[:] = []
    ctrls_a[:] = []

    ctrl = sel[1]
    jnt_parent = sel[0]
    jnts = cmds.listRelatives(jnt_parent, ad=True, type="joint")[:-1]
    ctrls = []
    for jnt in jnts:
        jntCtrl = cmds.duplicate(ctrl, name=jnt.replace('jnt_', 'ctrl_'))[0]
        cmds.matchTransform(jntCtrl, jnt, position=True, rotation=True)
        jntCtrl_name = jnt.replace('jnt_', 'ctrl_')
        cmds.rename(jntCtrl, jntCtrl_name)
        jntCtrl = [jntCtrl]
        _ctrl_hiracchy(jntCtrl)
        ctrls.append(jntCtrl[0])

    for zero in zeros:
        name_parts = zero.split('_')
        numb = int(name_parts[3])
        numb_father = numb - 1
        ctrl_father = 'ctrl_{}_{}_{:03d}'.format(name_parts[1], name_parts[2], numb_father)

        if cmds.objExists(ctrl_father):
            cmds.parent(zero, ctrl_father)

    for ctrl in ctrls:
        jnt = ctrl.replace('ctrl_', 'jnt_')
        if cmds.objExists(jnt):
            node = cmds.parentConstraint(ctrl, jnt, maintainOffset=True)[0]
            cmds.setAttr(node + '.interpType', 2)
def run_parent_mesh_to_ctrl_grp():
    """选择模型，再加选需要跟随的关节，自动检测该关节是否已有控制组，
    有则把模型 p 进去，没有则创建一个匹配关节的控制组。"""
    sel = cmds.ls(selection=True)
    if len(sel) < 2:
        cmds.warning("请先选择模型，再加选需要跟随的关节。")
        return

    meshs = sel[:-1]
    jnt = sel[0]

    nameParts = jnt.split('_')
    name_grp = "grp_{}_{}CtrlGrp_{}".format(nameParts[1], nameParts[2], nameParts[3])
    if cmds.objExists(name_grp):
        for mesh in meshs:
            cmds.parent(mesh, name_grp)

    else:
        cmds.createNode("transform", n=name_grp)
        cmds.matchTransform(name_grp, jnt, position=True, rotation=True)
def run_ctrl_joints_from_curves():
    """给选中的曲线（5 个 CV 点）在两端与中心生成三个主控制关节，
    注意生成的关节朝向为默认朝向，需手动调整。"""
    ctrl_crvs = cmds.ls(selection=True)
    if not ctrl_crvs:
        cmds.warning("请先选择 5 个 CV 点的曲线。")
        return

    locs_cv1n2 = []
    locs_cv3 = []
    locs_cv4n5 = []
    for i in range(2):
        for ctrl_crv in ctrl_crvs:
            cv_position = cmds.pointPosition(ctrl_crv + '.cv[{}]'.format(i), w=True)

            loc = cmds.spaceLocator()[0]
            cmds.xform(loc, ws=True, translation=cv_position)
            locs_cv1n2.append(loc)

    name_parts = ctrl_crvs[0].split('_')
    name_side = name_parts[1]
    name_description = name_parts[2]
    name_index = int(name_parts[3])
    ctrl_jnt_up = cmds.createNode('joint', name='jnt_{}_{}_{:03d}'.format(name_side,
                                                                          name_description, name_index))
    node_pc = cmds.pointConstraint(locs_cv1n2, ctrl_jnt_up, maintainOffset=False)[0]
    cmds.delete(node_pc)
    for loc in locs_cv1n2:
        cmds.delete(loc)

    for ctrl_crv in ctrl_crvs:
        cv_position = cmds.pointPosition(ctrl_crv + '.cv[2]', w=True)

        loc = cmds.spaceLocator()[0]
        cmds.xform(loc, ws=True, translation=cv_position)
        locs_cv3.append(loc)

    ctrl_jnt_mid = cmds.createNode('joint', name='jnt_{}_{}_{:03d}'.format(name_side,
                                                                           name_description, name_index + 1))
    node_pc = cmds.pointConstraint(locs_cv3, ctrl_jnt_mid, maintainOffset=False)[0]
    cmds.delete(node_pc)
    for loc in locs_cv3:
        cmds.delete(loc)

    for i in [3, 4]:
        for ctrl_crv in ctrl_crvs:
            cv_position = cmds.pointPosition(ctrl_crv + '.cv[{}]'.format(i), w=True)

            loc = cmds.spaceLocator()[0]
            cmds.xform(loc, ws=True, translation=cv_position)
            locs_cv4n5.append(loc)

    ctrl_jnt_down = cmds.createNode('joint', name='jnt_{}_{}_{:03d}'.format(name_side,
                                                                            name_description, name_index + 2))
    node_pc = cmds.pointConstraint(locs_cv4n5, ctrl_jnt_down, maintainOffset=False)[0]
    cmds.delete(node_pc)
    for loc in locs_cv4n5:
        cmds.delete(loc)
def run_picker_to_ctrl():
    """先选 picker 曲线，再加选控制器，把 picker 形状固定到控制器上（跟随控制器但不随其旋转位移而变形）。"""
    sel = cmds.ls(selection=True)
    if len(sel) < 2:
        cmds.warning("请先选 picker 曲线，再加选控制器。")
        return

    son = sel[0]
    father = sel[1]

    orig_transform = cmds.duplicate(son)
    shape_picker = cmds.listRelatives(son, shapes=True)[0]
    jnt_pin = cmds.createNode('joint')
    cmds.matchTransform(jnt_pin, orig_transform)
    cmds.skinCluster(orig_transform, jnt_pin, toSelectedBones=True, mi=5, obeyMaxInfluences=True)
    cmds.delete(jnt_pin)
    shape_orig = cmds.listRelatives(orig_transform, shapes=True)[1]
    if 'Orig' in shape_orig:
        cmds.parent(shape_picker, father, add=True, shape=True)
        cmds.parent(shape_orig, father, add=True, shape=True)
        cmds.delete(son)
        cmds.delete(orig_transform)

        TG_node = cmds.createNode('transformGeometry')
        cmds.connectAttr(father + '.inverseMatrix', TG_node + '.transform')
        cmds.connectAttr(shape_orig + '.worldSpace', TG_node + '.inputGeometry')
        cmds.connectAttr(TG_node + '.outputGeometry', shape_picker + '.create')
def space_attr(ctrls, attrs):
    """给 ctrls 添加 space 属性，属性里有 attrs 的选项（adv 命名格式）。

    :param ctrls: (list) 控制器。
    :param attrs: (list) 选项：chest, root, world, shoulder, hip。
    """
    enum_string = ':'.join(attrs)

    space_jnts = []
    for ctrl in ctrls:
        name_parts = ctrl.split('_')
        name_side = name_parts[1]
        name_des = name_parts[0]
        name_description = name_des[2:]
        name_FIK = name_des[:2]

        cmds.addAttr(ctrl, longName='space', attributeType='enum', enumName=enum_string)
        cmds.setAttr(ctrl + '.space', keyable=True)
        extra_grp = cmds.listRelatives(ctrl, parent=True)[0]
        offset_grp = cmds.listRelatives(extra_grp, parent=True)[0]
        space_grp = cmds.createNode('transform', name='{}Space{}_{}'.format(name_FIK, name_description, name_side))
        cmds.matchTransform(space_grp, ctrl)
        cmds.parent(space_grp, offset_grp)
        cmds.parent(extra_grp, space_grp)

        for attr in attrs:
            if attr == 'chest':
                space_jnts.append('Chest_M')

            if attr == 'root':
                space_jnts.append('Root_M')

            if attr == 'world':
                space_jnts.append('grp_m_spaceWorld_001')

            if attr == 'shoulder':
                if name_side == 'L':
                    space_jnts.append('Shoulder_L')

                if name_side == 'R':
                    space_jnts.append('Shoulder_R')

            if attr == 'hip':
                if name_side == 'L':
                    space_jnts.append('Hip_L')

                if name_side == 'R':
                    space_jnts.append('Hip_R')

        par_cons_node = cmds.parentConstraint(space_jnts, space_grp, maintainOffset=True)[0]
        cmds.setAttr(par_cons_node + '.interpType', 2)

        i = 0
        for attr in attrs:
            cond = cmds.createNode('condition', name='cond_{}_{}{}_001'.format(name_side.lower(), name_description,
                                                                               attr.capitalize()))
            cmds.connectAttr(ctrl + '.space', cond + '.firstTerm')
            cmds.setAttr(cond + '.secondTerm', i)
            cmds.setAttr(cond + '.operation', 0)
            cmds.setAttr(cond + '.colorIfTrueR', 1)
            cmds.setAttr(cond + '.colorIfFalseR', 0)
            cmds.connectAttr(cond + '.outColorR', par_cons_node + '.{}W{}'.format(space_jnts[i], i))
            i += 1


def run_ctrl_space_switch():
    """选中控制器（可多选），添加 space 空间切换属性。"""
    ctrls = cmds.ls(selection=True)
    if not ctrls:
        cmds.warning("请先选择控制器。")
        return

    result = prompt_fields(
        "空间切换",
        [("选项 (空格分隔: chest root world shoulder hip)", "chest root world")])
    if result is None:
        return

    attrs = result[0].split()

    space_attr(ctrls, attrs)
def run_mesh_match_joint_name():
    """选中已命名的骨骼（格式 description + L/R），再加选需要更名的模型，
    在世界原点新建命好名的空白组，把模型 p 进去并放回原层级，再用关节父子+缩放约束该组。"""
    sel = cmds.ls(selection=True)
    if len(sel) < 2:
        cmds.warning("请先选骨骼，再加选需要匹配的模型。")
        return

    jnt = sel[0]
    meshes = sel[1:]
    name_parts = jnt.split('_')
    description = name_parts[0]
    direction = name_parts[1]
    if direction == 'R':
        grp_dir = 'r_'

    elif direction == 'L':
        grp_dir = 'l_'

    else:
        grp_dir = 'm_'

    mesh_grp_name = 'grp_' + grp_dir + description + '_001'
    if cmds.objExists(mesh_grp_name):
        for mesh in meshes:
            mesh_parent = cmds.listRelatives(mesh, parent=True)[0]
            if mesh_parent == mesh_grp_name:
                if cmds.objExists(mesh_parent + '_parentConstraint1'):
                    cmds.warning('Object already exists')
                else:
                    cmds.parentConstraint(jnt, mesh_parent, maintainOffset=True)
                    cmds.scaleConstraint(jnt, mesh_parent)

            else:
                cmds.parent(mesh, mesh_grp_name)

    else:
        mesh_grp = cmds.createNode('transform', name=mesh_grp_name)
        mesh_parent = cmds.listRelatives(meshes[0], parent=True)[0]
        cmds.parent(mesh_grp, mesh_parent)

        cmds.parentConstraint(jnt, mesh_grp, maintainOffset=True)
        cmds.scaleConstraint(jnt, mesh_grp, maintainOffset=True)
        for mesh in meshes:
            cmds.parent(mesh, mesh_grp)
def run_constraint_grp_to_skincluster():
    """选中需要转蒙皮的 grp 组（组名格式 grp_l_description_001），
    根据组名推断对应关节（description + L/R），对组下模型做蒙皮，把约束驱动改为蒙皮驱动。"""
    grps = cmds.ls(selection=True)
    if not grps:
        cmds.warning("请先选择 grp 组。")
        return

    for grp in grps:
        name_parts = grp.split('_')
        description = name_parts[2]
        side = name_parts[1]
        jnt = description + '_' + side.upper()
        meshes = cmds.listRelatives(grp, children=True)
        for mesh in meshes:
            cmds.skinCluster(mesh, jnt, toSelectedBones=True, mi=5, obeyMaxInfluences=True)
def run_transfer_skin_weights():
    """先选有权重的模型，再加选目标模型，把源模型的蒙皮权重拷贝到目标模型上。"""
    sel = cmds.ls(selection=True)
    if len(sel) < 2:
        cmds.warning("请先选有权重的模型，再加选目标模型。")
        return

    source_mesh = sel[0]
    target_meshes = sel[1:]

    source_skin = mel.eval('findRelatedSkinCluster("' + source_mesh + '")')

    source_joints = cmds.skinCluster(source_skin, query=True, influence=True)

    for target_mesh in target_meshes:
        target_skin = cmds.skinCluster(source_joints, target_mesh, toSelectedBones=True)[0]
        cmds.copySkinWeights(sourceSkin=source_skin, destinationSkin=target_skin, noMirror=True,
                             surfaceAssociation='closestPoint', influenceAssociation=['label', 'oneToOne'])
        cmds.select(target_mesh)
        cmds.RemoveUnusedInfluences()
        if target_mesh.startswith('mesh_'):
            cmds.rename(target_skin, target_mesh.replace('mesh_', 'skinCluster_'))
def attach_jnts_on_curve(curve, jnts=None):
    """给指定曲线上所有 CV 点生成钉在上面的骨骼；若给定 jnts，则把给定骨骼钉在曲线上。

    :param curve: (str) 曲线。
    :param jnts: (list/None) 已有骨骼列表，可选。
    """
    curve_shape = cmds.listRelatives(curve, shapes=True)[0]
    if jnts:
        for jnt in jnts:
            npoc = cmds.createNode('nearestPointOnCurve')
            cmds.connectAttr(curve_shape + '.worldSpace[0]', npoc + '.inputCurve')
            cmds.connectAttr(jnt + '.translate', npoc + '.inPosition')
            parameter = cmds.getAttr(npoc + '.parameter')
            cmds.delete(npoc)

            poci = cmds.createNode('pointOnCurveInfo')
            cmds.connectAttr(curve_shape + '.worldSpace[0]', poci + '.inputCurve')
            cmds.setAttr(poci + '.parameter', parameter)
            cmds.connectAttr(poci + '.position', jnt + '.translate')

    else:
        jnts = []
        cvs = cmds.ls(curve + '.cv[*]', flatten=True)
        for cv in cvs:
            position = cmds.xform(cv, query=True, worldSpace=True, translation=True)

            joint = cmds.createNode('joint')
            cmds.xform(joint, translation=position)
            jnts.append(joint)

        for jnt in jnts:
            npoc = cmds.createNode('nearestPointOnCurve')
            cmds.connectAttr(curve_shape + '.worldSpace[0]', npoc + '.inputCurve')
            cmds.connectAttr(jnt + '.translate', npoc + '.inPosition')
            parameter = cmds.getAttr(npoc + '.parameter')
            cmds.delete(npoc)

            poci = cmds.createNode('pointOnCurveInfo')
            cmds.connectAttr(curve_shape + '.worldSpace[0]', poci + '.inputCurve')
            cmds.setAttr(poci + '.parameter', parameter)
            cmds.connectAttr(poci + '.position', jnt + '.translate')


def run_attach_joints_on_cv():
    """选中一条曲线（可选加选已有骨骼），把骨骼钉在曲线的 CV 点上；
    若只选曲线则按 CV 点生成新骨骼。"""
    sel = cmds.ls(selection=True)
    if not sel:
        cmds.warning("请先选择一条曲线。")
        return

    curve = sel[0]
    jnts = sel[1:] if len(sel) > 1 else None
    attach_jnts_on_curve(curve, jnts)
def attach_joints_on_curve(joints, drive_curve, aim_curve, up_object, aim_type='object'):
    """使用 pointOnCurveInfo 节点把关节附着到曲线上。

    :param joints: (list) 需要附着的关节。
    :param drive_curve: (str) 驱动关节的曲线。
    :param aim_curve: (str) 驱动 aiming 节点的曲线。
    :param up_object: (str) 关节的上方向参考物体。
    :param aim_type: (str) object/curve。
    """
    name_parts = drive_curve.split('_')
    name_side = name_parts[1]
    name_description = name_parts[2]
    name_index = int(name_parts[3])

    if aim_type == 'curve':
        curves = [drive_curve, aim_curve, up_object]
    else:
        curves = [drive_curve, aim_curve]

    grp_jnts = cmds.createNode('transform', name='grp_{}_{}Jnts_{:03d}'.format(name_side, name_description,
                                                                               name_index))

    grp_nodes = cmds.createNode('transform',
                                name='grp_{}_{}RigNodes_{:03d}'.format(name_side, name_description, name_index))

    grp_attaches = []
    for crv, part_name in zip(curves, ['Drive', 'Aim', 'Up']):
        grp_attach = cmds.createNode('transform',
                                     name='grp_{}_{}{}Attaches_{:03d}'.format(name_side, name_description, part_name,
                                                                              name_index),
                                     parent=grp_nodes)
        grp_attaches.append(grp_attach)

    cmds.parent(curves, grp_nodes)

    crv_shapes = []
    for crv in curves:
        crv_shape = cmds.listRelatives(crv, shapes=True)[0]
        crv_shapes.append(crv_shape)

    for jnt in joints:
        jnt_name_parts = jnt.split('_')
        jnt_side = jnt_name_parts[1]
        jnt_description = jnt_name_parts[2]
        jnt_index = int(jnt_name_parts[3])

        npoc = cmds.createNode('nearestPointOnCurve')
        cmds.connectAttr(crv_shapes[0] + '.worldSpace[0]', npoc + '.inputCurve')
        cmds.connectAttr(jnt + '.translate', npoc + '.inPosition')
        parameter = cmds.getAttr(npoc + '.parameter')
        cmds.delete(npoc)

        attach_nodes = []
        for crv_shape, part_name, grp in zip(crv_shapes, ['Drive', 'Aim', 'Up'], grp_attaches):
            attach = cmds.createNode('transform',
                                     name='grp_{}_{}{}Attach_{:03d}'.format(jnt_side, jnt_description, part_name,
                                                                             jnt_index),
                                     parent=grp)
            poci = cmds.createNode('pointOnCurveInfo', name=attach.replace('grp', 'poci'))
            cmds.connectAttr(crv_shape + '.worldSpace[0]', poci + '.inputCurve')
            cmds.setAttr(poci + '.parameter', parameter)
            cmds.connectAttr(poci + '.position', attach + '.translate')
            attach_nodes.append(attach)

        if aim_type == 'curve':
            cmds.aimConstraint(attach_nodes[1], attach_nodes[0], aimVector=[1, 0, 0], upVector=[0, 1, 0],
                               worldUpType='object', worldUpObject=attach_nodes[2], worldUpVector=[0, 1, 0],
                               maintainOffset=False)
        else:
            cmds.aimConstraint(attach_nodes[1], attach_nodes[0], aimVector=[1, 0, 0], upVector=[0, 1, 0],
                               worldUpType='objectrotation', worldUpObject=up_object, worldUpVector=[0, 1, 0],
                               maintainOffset=False)

        zero = cmds.createNode('transform', name=jnt.replace('jnt', 'zero'), parent=grp_jnts)
        cmds.parentConstraint(attach_nodes[0], zero, maintainOffset=False)

        cmds.parent(jnt, zero)
        cmds.matchTransform(jnt, zero, position=False, rotation=True)
        cmds.makeIdentity(jnt, apply=True, translate=True, rotate=True, scale=True)


def run_attach_joints_aim_curve():
    """选中需要附着的关节，再依次选 drive 曲线、aim 曲线、上方向参考物体，
    用 pointOnCurveInfo 把关节附着到曲线上并 aim 约束。"""
    sel = cmds.ls(selection=True)
    if len(sel) < 3:
        cmds.warning("请选择：关节(可多选) + drive曲线 + aim曲线 + 上方向参考物体。")
        return

    joints = sel[:-3]
    drive_curve = sel[-3]
    aim_curve = sel[-2]
    up_object = sel[-1]

    result = prompt_fields(
        "关节附着到曲线",
        [("aim_type (object/curve)", "object")])
    if result is None:
        return
    aim_type = result[0].strip() or 'object'

    attach_joints_on_curve(joints, drive_curve, aim_curve, up_object, aim_type=aim_type)
def curve_joints(curve, up_curve, joint_number=10):
    """把关节附着到给定曲线上，用曲线切线作为主轴，上方向指向 up_curve。

    :param curve: (str) 驱动关节的曲线。
    :param up_curve: (str) 定义上方向的曲线。
    :param joint_number: (int) 附着的关节数量，默认 10。
    """
    name_parts = curve.split('_')
    name_side = name_parts[1]
    name_description = name_parts[2]
    name_index = int(name_parts[3])

    rig_grp = cmds.createNode('transform', name='grp_{}_{}Rig_{:03d}'.format(name_side, name_description, name_index))
    jnt_grp = cmds.createNode('transform', name='grp_{}_{}Jnts_{:03d}'.format(name_side, name_description, name_index),
                              parent=rig_grp)
    node_grp = cmds.createNode('transform', name='grp_{}_{}Nodes_{:03d}'.format(name_side, name_description,
                                                                                name_index),
                               parent=rig_grp)

    cmds.parent(curve, up_curve, node_grp)

    curve_shape = cmds.listRelatives(curve, shapes=True)[0]
    up_shape = cmds.listRelatives(up_curve, shapes=True)[0]

    for i in range(joint_number):
        u_val = float(i) / (joint_number - 1)
        poci_nodes = []
        for shape, suffix in zip([curve_shape, up_shape], ['', 'Up']):
            poci = cmds.createNode('pointOnCurveInfo',
                                   name='poci_{}_{}Jnt{}_{:03d}'.format(name_side, name_description, suffix, i + 1))

            cmds.connectAttr(shape + '.worldSpace[0]', poci + '.inputCurve')
            cmds.setAttr(poci + '.parameter', u_val)
            poci_nodes.append(poci)

        vec_up = cmds.createNode('plusMinusAverage',
                                 name='vecUp_{}_{}Jnt_{:03d}'.format(name_side, name_description, i + 1))
        cmds.setAttr(vec_up + '.operation', 2)
        cmds.connectAttr(poci_nodes[1] + '.position', vec_up + '.input3D[0]')
        cmds.connectAttr(poci_nodes[0] + '.position', vec_up + '.input3D[1]')

        vec_z = cmds.createNode('vectorProduct',
                                name='vecZ_{}_{}Jnt_{:03d}'.format(name_side, name_description, i + 1))
        cmds.setAttr(vec_z + '.operation', 2)
        cmds.connectAttr(poci_nodes[0] + '.tangent', vec_z + '.input1')
        cmds.connectAttr(vec_up + '.output3D', vec_z + '.input2')
        cmds.setAttr(vec_z + '.normalizeOutput', 1)

        vec_y = cmds.createNode('vectorProduct',
                                name='vecY_{}_{}Jnt_{:03d}'.format(name_side, name_description, i + 1))
        cmds.setAttr(vec_y + '.operation', 2)
        cmds.connectAttr(vec_z + '.output', vec_y + '.input1')
        cmds.connectAttr(poci_nodes[0] + '.tangent', vec_y + '.input2')
        cmds.setAttr(vec_y + '.normalizeOutput', 1)

        matrix = cmds.createNode('fourByFourMatrix',
                                 name='matrix4x4_{}_{}Jnt_{:03d}'.format(name_side, name_description, i + 1))

        cmds.connectAttr(poci_nodes[0] + '.normalizedTangentX', matrix + '.in00')
        cmds.connectAttr(poci_nodes[0] + '.normalizedTangentY', matrix + '.in01')
        cmds.connectAttr(poci_nodes[0] + '.normalizedTangentZ', matrix + '.in02')

        cmds.connectAttr(vec_y + '.outputX', matrix + '.in10')
        cmds.connectAttr(vec_y + '.outputY', matrix + '.in11')
        cmds.connectAttr(vec_y + '.outputZ', matrix + '.in12')

        cmds.connectAttr(vec_z + '.outputX', matrix + '.in20')
        cmds.connectAttr(vec_z + '.outputY', matrix + '.in21')
        cmds.connectAttr(vec_z + '.outputZ', matrix + '.in22')

        cmds.connectAttr(poci_nodes[0] + '.positionX', matrix + '.in30')
        cmds.connectAttr(poci_nodes[0] + '.positionY', matrix + '.in31')
        cmds.connectAttr(poci_nodes[0] + '.positionZ', matrix + '.in32')

        decompose = cmds.createNode('decomposeMatrix',
                                    name='decomposeMatrix_{}_{}Jnt_{:03d}'.format(name_side, name_description, i + 1))
        cmds.connectAttr(matrix + '.output', decompose + '.inputMatrix')

        jnt = cmds.createNode('joint', name='jnt_{}_{}_{:03d}'.format(name_side, name_description, i + 1),
                              parent=jnt_grp)
        cmds.connectAttr(decompose + '.outputTranslate', jnt + '.translate')
        cmds.connectAttr(decompose + '.outputRotate', jnt + '.rotate')


def run_joints_on_curve_tangent():
    """选中驱动曲线，再加选上方向曲线，生成沿曲线切线朝向的骨骼链。"""
    sel = cmds.ls(selection=True)
    if len(sel) < 2:
        cmds.warning("请先选驱动曲线，再加选上方向曲线。")
        return

    curve = sel[0]
    up_curve = sel[1]

    result = prompt_fields(
        "曲线生成骨骼",
        [("骨骼数量", "10")])
    if result is None:
        return
    joint_number = int(result[0]) if result[0].strip().isdigit() else 10

    curve_joints(curve, up_curve, joint_number)
def curve_easy_bind(curve):
    """给 curve 生成一条重建后有 5 个 CV 点的新曲线作为控制曲线。

    :param curve: (str) 原始曲线。
    :return: (str) 重建后的控制曲线。
    """
    name_parts = curve.split('_')
    name_side = name_parts[1]
    name_description = name_parts[2]
    name_index = int(name_parts[3])

    ctrl_crv = cmds.duplicate(curve, name='crv_{}_{}Ctrl_{:03d}'.format(name_side, name_description, name_index))[0]
    ctrl_crv = cmds.rebuildCurve(ctrl_crv, ch=1, rpo=1, rt=0, end=1, kr=0,
                                 kcp=0, kep=1, kt=0, s=3, d=2, tol=0.01)[0]

    return ctrl_crv


def tube_rig(drive_curve, up_curve, ctrl_jnts=None, mirror=True):
    """管状绑定：驱动曲线 + 上方向曲线生成附着骨骼，可镜像。

    :param drive_curve: (str) 驱动曲线。
    :param up_curve: (str) 上方向曲线。
    :param ctrl_jnts: (list/None) 三个控制骨骼，可选。
    :param mirror: (bool) 是否执行镜像。
    """
    name_parts = drive_curve.split('_')
    name_side = name_parts[1]
    name_description = name_parts[2]
    name_description = name_description.replace('Drive', 'Tube')
    name_index = int(name_parts[3])

    curves = [drive_curve, up_curve]
    grp_nodes = cmds.createNode('transform',
                                name='grp_{}_{}RigNodes_{:03d}'.format(name_side, name_description, name_index))

    grp_attaches = []
    for crv, part_name in zip(curves, ['Drive', 'Up']):
        grp_attach = cmds.createNode('transform',
                                     name='grp_{}_{}{}Attaches_{:03d}'.format(name_side, name_description,
                                                                              part_name, name_index),
                                     parent=grp_nodes)
        grp_attaches.append(grp_attach)

    cmds.parent(curves, grp_nodes)

    crv_shapes = []
    for crv in curves:
        crv_shape = cmds.listRelatives(crv, shapes=True)[0]
        crv_shapes.append(crv_shape)

    cvs = cmds.ls(drive_curve + '.cv[*]', flatten=True)

    jnts = []
    jnt_val = 1
    for cv in cvs:
        position = cmds.xform(cv, query=True, worldSpace=True, translation=True)

        joint = cmds.createNode('joint', name='jnt_{}_{}Bind_{:03d}'.format(name_side, name_description, jnt_val))
        jnt_val += 1
        cmds.xform(joint, translation=position)
        jnts.append(joint)

    for jnt in jnts:
        npoc = cmds.createNode('nearestPointOnCurve')
        cmds.connectAttr(crv_shapes[0] + '.worldSpace[0]', npoc + '.inputCurve')
        cmds.connectAttr(jnt + '.translate', npoc + '.inPosition')
        parameter = cmds.getAttr(npoc + '.parameter')
        cmds.delete(npoc)

        attach_nodes = []
        for crv_shape, part_name, grp in zip(crv_shapes, ['Drive', 'Up'], grp_attaches):
            attach = cmds.createNode('transform',
                                     name='grp_{}_{}Attach{}_{:03d}'.format(name_side, name_description,
                                                                             part_name, name_index),
                                     parent=grp)
            poci = cmds.createNode('pointOnCurveInfo', name=attach.replace('grp', 'poci'))
            cmds.connectAttr(crv_shape + '.worldSpace[0]', poci + '.inputCurve')
            cmds.setAttr(poci + '.parameter', parameter)
            cmds.connectAttr(poci + '.position', attach + '.translate')
            attach_nodes.append(attach)

    grp_pars = cmds.createNode('transform', name='grp_{}_{}_001'.format(name_side,
                                                                        name_description.replace('Tube', 'Par')))
    cmds.parent(grp_pars, grp_nodes)
    for jnt in jnts:
        name_parts = jnt.split('_')
        name_side = name_parts[1]
        name_description = name_parts[2]
        name_description = name_description[:-4]
        name_index = int(name_parts[3])

        attach_drive = 'grp_{}_{}AttachDrive_{:03d}'.format(name_side, name_description,
                                                            name_index)
        attach_up = 'grp_{}_{}AttachUp_{:03d}'.format(name_side, name_description,
                                                      name_index)
        aim_jnt = 'jnt_{}_{}Bind_{:03d}'.format(name_side, name_description, name_index + 1)
        grp_parent = cmds.createNode('transform', name='grp_{}_{}Par_{:03d}'.format(name_side, name_description,
                                                                                    name_index))
        cmds.parent(grp_parent, grp_pars)
        cmds.pointConstraint(attach_drive, grp_parent, maintainOffset=False)
        if cmds.objExists(aim_jnt):
            cmds.aimConstraint(aim_jnt, grp_parent, aimVector=[1, 0, 0], upVector=[0, 1, 0],
                               worldUpType='object', worldUpObject=attach_up, worldUpVector=[0, 1, 0],
                               maintainOffset=True)
        cmds.parentConstraint(grp_parent, jnt, maintainOffset=True)

    ctrl_crvs = []
    for curve in curves:
        ctrl_crv = curve_easy_bind(curve)
        ctrl_crvs.append(ctrl_crv)

    if ctrl_jnts:
        for ctrl_crv in ctrl_crvs:
            cmds.skinCluster(ctrl_jnts, ctrl_crv, tsb=True)

    if mirror:
        ctrl_jnts_mirror = None
        if name_side == 'l':
            name_side_mirror = 'r'
            if ctrl_jnts:
                ctrl_jnts_mirror = []
                for ctrl_jnt in ctrl_jnts:
                    ctrl_jnt_mirror = cmds.mirrorJoint(ctrl_jnt, mirrorYZ=True,
                                                       mirrorBehavior=True, searchReplace=['_l_', '_r_'])[0]
                    ctrl_jnts_mirror.append(ctrl_jnt_mirror)
        else:
            name_side_mirror = 'l'
            if ctrl_jnts:
                ctrl_jnts_mirror = []
                for ctrl_jnt in ctrl_jnts:
                    ctrl_jnt_mirror = cmds.mirrorJoint(ctrl_jnt, mirrorYZ=True,
                                                       mirrorBehavior=True, searchReplace=['_r_', '_l_'])[0]
                    ctrl_jnts_mirror.append(ctrl_jnt_mirror)

        drive_curve_mirror_name = drive_curve.replace('_' + name_side + '_', '_' + name_side_mirror + '_')
        up_curve_mirror_name = up_curve.replace('_' + name_side + '_', '_' + name_side_mirror + '_')

        drive_curve_mirror = cmds.duplicate(drive_curve, name=drive_curve_mirror_name)[0]
        up_curve_mirror = cmds.duplicate(up_curve, name=up_curve_mirror_name)[0]

        curves_mirror = [drive_curve_mirror, up_curve_mirror]
        mirror_grp = cmds.createNode('transform')
        for curve_mirror in curves_mirror:
            cmds.parent(curve_mirror, mirror_grp)

        cmds.setAttr(mirror_grp + '.sx', -1)
        cmds.makeIdentity(mirror_grp, apply=True, scale=True)

        for curve_mirror in curves_mirror:
            cmds.parent(curve_mirror, world=True)

        cmds.delete(mirror_grp)

        tube_rig(drive_curve_mirror, up_curve_mirror, ctrl_jnts_mirror, mirror=False)


def run_tube_rig():
    """选中驱动曲线，再加选上方向曲线（可再加选 3 个控制骨骼），执行管状绑定。"""
    sel = cmds.ls(selection=True)
    if len(sel) < 2:
        cmds.warning("请先选驱动曲线，再加选上方向曲线。")
        return

    drive_curve = sel[0]
    up_curve = sel[1]
    ctrl_jnts = sel[2:] if len(sel) > 2 else None

    result = prompt_fields(
        "管状绑定",
        [("镜像 (1/0)", "1")])
    if result is None:
        return
    mirror = bool(int(result[0]) if result[0].strip() in ('0', '1') else 1)

    tube_rig(drive_curve, up_curve, ctrl_jnts, mirror)
crvs_return = []


def tiger_beard(ctrls, drive_curve, aim_curve, up_curve, mirror=True):
    """用 aim_curve 驱动主控制器（胡须/触须绑定），可镜像。

    :param ctrls: (list) 所有需要用 aim_curve 驱动的主控制器。
    :param drive_curve: (str) 驱动曲线。
    :param aim_curve: (str) 目标曲线。
    :param up_curve: (str) 上方向曲线。
    :param mirror: (bool) 是否执行镜像。
    """
    global crvs_return
    crvs_return.append(aim_curve)
    crvs_return.append(up_curve)

    name_parts = drive_curve.split('_')
    name_side = name_parts[1]
    name_description = name_parts[2]
    name_index = int(name_parts[3])

    curves = [drive_curve, aim_curve, up_curve]

    grp_nodes = cmds.createNode('transform',
                                name='grp_{}_{}RigNodes_{:03d}'.format(name_side, name_description, name_index))

    grp_attaches = []
    for crv, part_name in zip(curves, ['Drive', 'Aim', 'Up']):
        grp_attach = cmds.createNode('transform',
                                     name='grp_{}_{}{}Attaches_{:03d}'.format(name_side, name_description,
                                                                              part_name, name_index),
                                     parent=grp_nodes)
        grp_attaches.append(grp_attach)

    cmds.parent(curves, grp_nodes)

    crv_shapes = []
    for crv in curves:
        crv_shape = cmds.listRelatives(crv, shapes=True)[0]
        crv_shapes.append(crv_shape)

    for ctrl in ctrls:
        ctrl_name_parts = ctrl.split('_')
        ctrl_side = ctrl_name_parts[1]
        ctrl_description = ctrl_name_parts[0]

        grp_extra = cmds.listRelatives(ctrl, parent=True)
        grp_offset = cmds.listRelatives(grp_extra, parent=True)
        grp_driven = cmds.createNode('transform', name=ctrl.replace('FK', 'FKDriven'))
        cmds.matchTransform(grp_driven, grp_extra)
        cmds.parent(grp_driven, grp_offset)
        cmds.parent(grp_extra, grp_driven)

        jnt = cmds.spaceLocator(name='jnt_{}_{}_{:3d}'.format(ctrl_side, ctrl_description, 1))[0]

        cmds.matchTransform(jnt, ctrl, position=True, rotation=True)

        npoc = cmds.createNode('nearestPointOnCurve')
        cmds.connectAttr(crv_shapes[0] + '.worldSpace[0]', npoc + '.inputCurve')
        cmds.connectAttr(jnt + '.translate', npoc + '.inPosition')
        parameter = cmds.getAttr(npoc + '.parameter')
        cmds.delete(npoc)

        attach_nodes = []
        for crv_shape, part_name, grp in zip(crv_shapes, ['Drive', 'Aim', 'Up'], grp_attaches):
            attach = cmds.createNode('transform',
                                     name='grp_{}_{}{}Attach_{:03d}'.format(ctrl_side, ctrl_description, part_name,
                                                                             1),
                                     parent=grp)
            poci = cmds.createNode('pointOnCurveInfo', name=attach.replace('grp', 'poci'))
            cmds.connectAttr(crv_shape + '.worldSpace[0]', poci + '.inputCurve')
            cmds.setAttr(poci + '.parameter', parameter)
            cmds.connectAttr(poci + '.position', attach + '.translate')
            attach_nodes.append(attach)

        cmds.aimConstraint(attach_nodes[1], attach_nodes[0], aimVector=[1, 0, 0], upVector=[0, 1, 0],
                           worldUpType='object', worldUpObject=attach_nodes[2], worldUpVector=[0, 1, 0],
                           maintainOffset=False)

        cmds.parentConstraint(attach_nodes[0], grp_driven, maintainOffset=True)
        cmds.delete(jnt)

    if mirror:
        ctrls_mirror = []
        if name_side == 'l':
            name_side_mirror = 'r'
        else:
            name_side_mirror = 'l'

        for ctrl in ctrls:
            ctrl_mirror = ctrl.replace('_' + name_side.upper(), '_' + name_side_mirror.upper())
            ctrls_mirror.append(ctrl_mirror)

        drive_curve_mirror_name = drive_curve.replace('_' + name_side + '_', '_' + name_side_mirror + '_')
        aim_curve_mirror_name = aim_curve.replace('_' + name_side + '_', '_' + name_side_mirror + '_')
        up_curve_mirror_name = up_curve.replace('_' + name_side + '_', '_' + name_side_mirror + '_')

        drive_curve_mirror = cmds.duplicate(drive_curve, name=drive_curve_mirror_name)[0]
        aim_curve_mirror = cmds.duplicate(aim_curve, name=aim_curve_mirror_name)[0]
        up_curve_mirror = cmds.duplicate(up_curve, name=up_curve_mirror_name)[0]

        curves_mirror = [drive_curve_mirror, aim_curve_mirror, up_curve_mirror]
        mirror_grp = cmds.createNode('transform')
        for curve_mirror in curves_mirror:
            cmds.parent(curve_mirror, mirror_grp)

        cmds.setAttr(mirror_grp + '.sx', -1)
        cmds.makeIdentity(mirror_grp, apply=True, scale=True)

        for curve_mirror in curves_mirror:
            cmds.parent(curve_mirror, world=True)

        cmds.delete(mirror_grp)

        tiger_beard(ctrls_mirror, drive_curve_mirror, aim_curve_mirror, up_curve_mirror, mirror=False)
    return crvs_return


def curves_easy_bind(curves):
    """给 curves 里每条曲线生成重建后有 5 个 CV 点的新曲线，并用新曲线对原曲线做线性变形控制。"""
    ctrl_crvs = []
    for curve in curves:
        name_parts = curve.split('_')
        name_side = name_parts[1]
        name_description = name_parts[2]
        name_index = int(name_parts[3])

        ctrl_crv = cmds.duplicate(curve, name='crv_{}_{}Ctrl_{:03d}'.format(name_side, name_description,
                                                                            name_index))[0]
        ctrl_crv = cmds.rebuildCurve(ctrl_crv, ch=0, rpo=1, rt=0, end=1, kr=0,
                                     kcp=0, kep=1, kt=0, s=3, d=2, tol=0.01)[0]
        ctrl_crvs.append(ctrl_crv)

        wire_node = cmds.wire(curve, gw=False, en=1.000000, ce=0.000000, li=0.000000, w=ctrl_crv)[0]
        cmds.setAttr(wire_node + '.dropoffDistance[0]', 100)
    return ctrl_crvs


def run_tentacle_beard_rig():
    """选中需要驱动的主控制器(可多选)，再加选 drive 曲线、aim 曲线、up 曲线（最后三个），
    用 aim 曲线驱动控制器并生成线性变形的控制曲线。"""
    global crvs_return
    sel = cmds.ls(selection=True)
    if len(sel) < 4:
        cmds.warning("请选择：主控制器(可多选) + drive曲线 + aim曲线 + up曲线。")
        return

    ctrls = sel[:-3]
    drive_curve = sel[-3]
    aim_curve = sel[-2]
    up_curve = sel[-1]

    result = prompt_fields(
        "胡须/触须绑定",
        [("镜像 (1/0)", "1")])
    if result is None:
        return
    mirror = bool(int(result[0]) if result[0].strip() in ('0', '1') else 1)

    crvs_return = []
    curves = tiger_beard(ctrls, drive_curve, aim_curve, up_curve, mirror=mirror)
    curves_easy_bind(curves)
# 给人腿创建最简单的IK和FK控制器
# 首先场景里已经有了命好名的关节，三根骨骼（四个关节）
# 创建一个函数，函数会遍历选择的关节的所有子关节，然后会复制出新的IK关节链并对选择的关节和倒数第二节子关节做springIK，
# 并对倒数第二节关节和倒数第一节关节做一个scik
# ik关节链会被放在一个位于世界坐标原点的组里，然后用原关节链的父级关节约束ik关节链的最上层关节
# 再复制一个FK关节链
# 接着是控制器的创建与约束

# 函数的创建
def ctrl_hirachy(ctrls):
    CTRL_COLORS = {'m': 17,
                   'l': 6,
                   'r': 13}

    SUB_COLORS = {'m': 25,
                  'l': 15,
                  'r': 4}

    # loop in each ctrl and create hierarchy
    for ctrl in ctrls:
        # get name parts
        name_parts = ctrl.split('_')

        # create zero group
        zero = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'zero_'))
        # create driven group
        driven = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'driven_'), parent=zero)
        # create connect group
        connect = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'connect_'), parent=driven)
        # create offset group
        offset = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'offset_'), parent=connect)

        # snap to control position
        cmds.matchTransform(zero, ctrl, position=True, rotation=True)

        # parent control to offset group
        cmds.parent(ctrl, offset)

        # freeze transformation for controller
        cmds.makeIdentity(ctrl, apply=True, scale=True)
        # delete history
        cmds.select(ctrl)
        cmds.DeleteHistory()

        # show rotate order
        cmds.setAttr(ctrl + '.rotateOrder', channelBox=True)

        # lock hide unused attrs
        for attr in ['visibility']:
            for ctrl_node in [ctrl]:
                cmds.setAttr('{}.{}'.format(ctrl_node, attr), keyable=False, channelBox=False, lock=True)

        # set color
        for ctrl_node, col_idx in zip([ctrl], [CTRL_COLORS[name_parts[1]]]):
            # get shape node
            shape_node = cmds.listRelatives(ctrl_node, shapes=True)[0]
            # set color
            cmds.setAttr(shape_node + '.overrideEnabled', 1)
            cmds.setAttr(shape_node + '.overrideColor', col_idx)

def human_leg(jnt_thigh):
    """

    :param jnt_thigh: str, 马腿最上层的关节
    :return: none
    """

    jnt_thigh_parent = cmds.listRelatives(jnt_thigh, parent=True)[0]
    # 遍历jnt_thigh的子关节，把他们都装进一个列表
    jnts_src = cmds.listRelatives(jnt_thigh, ad=True, type="joint")
    jnts_src.reverse()
    jnts_src.insert(0,jnt_thigh)

    # 复制原始关节链并给新的关节description里加上ik
    jnts_ik = cmds.duplicate(jnt_thigh, renameChildren=True)

    # 创建一个空列表用于存放改好名字的spik关节
    jnts_ik_new = []

    for jnt_src, jnt_ik in zip(jnts_src, jnts_ik):
        nameParts = jnt_src.split('_')
        name_description_ik = nameParts[2] + "Ik"
        name_jnt_ik = "{}_{}_{}_{}".format(nameParts[0], nameParts[1], name_description_ik, nameParts[3])
        cmds.rename(jnt_ik, name_jnt_ik)
        jnts_ik_new.append(name_jnt_ik)

    # 把ik关节链放入一个位于世界坐标原点的组
    grp_name = jnts_ik_new[0].replace("jnt_", "jntGrp_")
    grp_ik = cmds.createNode('transform', name=grp_name)
    cmds.parent(jnts_ik_new[0], grp_ik)
    cmds.setAttr(grp_ik + ".visibility", 0)

    # 创建rpik
    ikHandle = cmds.ikHandle(sj=jnts_ik_new[0], ee=jnts_ik_new[-3])[0]

    # 创建控制器形状
    ctrl_name_ik = grp_name.replace("jntGrp_", "ctrl_")
    ctrl_ik = cmds.curve(d=1, p=[(-0.5, 0.5, 0.5), (0.5, 0.5, 0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5),
                                   (-0.5, 0.5, 0.5),(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5),
                                   (0.5, -0.5, 0.5), (0.5, -0.5, -0.5),(0.5, 0.5, -0.5), (0.5, -0.5, -0.5),
                                   (-0.5, -0.5, -0.5), (-0.5, 0.5, -0.5), (-0.5, -0.5, -0.5),(-0.5, -0.5, 0.5)],
                                    k=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],n=ctrl_name_ik)
    ctrl_hirachy([ctrl_ik])
    zeroGrp_ctrl_ik = ctrl_ik.replace("ctrl_", "zero_")
    cmds.matchTransform(zeroGrp_ctrl_ik, jnts_ik_new[-3], position=True, rotation=True)
    ctrl_shape = cmds.listRelatives(ctrl_ik, shapes=True)[0]
    cmds.select(ctrl_shape + ".cv[0]", ctrl_shape + ".cv[1]", ctrl_shape + ".cv[2]", ctrl_shape + ".cv[3]",
                ctrl_shape + ".cv[4]", ctrl_shape + ".cv[5]", ctrl_shape + ".cv[6]", ctrl_shape + ".cv[7]",
                ctrl_shape + ".cv[8]", ctrl_shape + ".cv[9]", ctrl_shape + ".cv[10]", ctrl_shape + ".cv[11]",
                ctrl_shape + ".cv[12]", ctrl_shape + ".cv[13]", ctrl_shape + ".cv[14]", ctrl_shape + ".cv[15]", )
    cmds.scale(16, 16, 16, r=True, ocp=True)

    # 把ik控制柄p给控制器并隐藏
    cmds.parent(ikHandle, ctrl_ik)
    cmds.setAttr(ikHandle + ".visibility", 0)

    # 创建一个loc作为极向量
    name_parts_new = jnts_ik_new[0].split('_')
    description_new = name_parts_new[2] + "Pole"
    loc_spik_name = "ctrl_{}_{}_{}".format(name_parts_new[1], description_new, name_parts_new[3])
    loc_spik = cmds.spaceLocator(name=loc_spik_name)[0]
    ctrl_hirachy([loc_spik])

    # 把极向量控制器和倒数第四个关节匹配位置
    zero_loc = loc_spik.replace("ctrl_", "zero_")
    cmds.parent(zero_loc, jnts_ik_new[-4])
    for attr in [".tx", ".ty", ".tz", ".rx", ".ry", ".rz"]:
        cmds.setAttr(zero_loc + attr, 0)

    # 把极向量的zero组沿当前父对象的y轴移动60个单位(如果是左腿，就是-60), 并p出关节
    if "_r_" in jnt_thigh:
        cmds.setAttr(zero_loc + ".ty", 60)
        cmds.parent(zero_loc, world=True)
    elif "_l_" in jnt_thigh:
        cmds.setAttr(zero_loc + ".ty", -60)
        cmds.parent(zero_loc, world=True)

    # 放大loc
    for attr in ["Shape.localScaleX", "Shape.localScaleY", "Shape.localScaleZ"]:
        cmds.setAttr(loc_spik + attr, 15)

    # 创建极向量约束
    cmds.poleVectorConstraint(loc_spik, ikHandle, weight=1)

    # 给末端一节骨骼创建scik
    scikHandle = cmds.ikHandle(sj=jnts_ik_new[-3], ee=jnts_ik_new[-2], sol="ikSCsolver")[0]
    cmds.parent(scikHandle, ctrl_ik)
    cmds.setAttr(scikHandle + ".visibility", 0)

    # 用jnt_thigh_parent约束spik关节
    if jnt_thigh_parent:
        constraint_node = cmds.parentConstraint(jnt_thigh_parent, jnts_ik_new[0], maintainOffset=True)[0]
        cmds.setAttr(constraint_node + ".interpType", 2)


    # IK控制系统已经设置完毕， 接下来是FK控制系统以及ikfk切换系统
    # 首先是FK系统
    # 复制原始关节链并给新的关节description里加上Fk
    jnts_fk = cmds.duplicate(jnt_thigh, renameChildren=True)

    # 创建一个空列表用于存放改好名字的fk关节
    jnts_fk_new = []

    for jnt_src, jnt_fk in zip(jnts_src, jnts_fk):
        nameParts = jnt_src.split('_')
        name_description_fk = nameParts[2] + "Fk"
        name_jnt_fk = "{}_{}_{}_{}".format(nameParts[0], nameParts[1], name_description_fk, nameParts[3])
        cmds.rename(jnt_fk, name_jnt_fk)
        jnts_fk_new.append(name_jnt_fk)

    # 把fk关节链也放到spik关节链的组下
    cmds.parent(jnts_fk_new[0], grp_ik)

    # 创建圆环控制器
    ctrl = cmds.circle( nr=(1, 0, 0), c=(0, 0, 0) )[0]
    fk_ctrl_shape = ctrl
    cmds.select(ctrl + ".cv[0]", ctrl + ".cv[1]", ctrl + ".cv[2]", ctrl + ".cv[3]", ctrl + ".cv[4]", ctrl + ".cv[5]",
                ctrl + ".cv[6]", ctrl + ".cv[7]")
    cmds.scale(9, 9, 9, r=True, ocp=True)

    # 给每个fk关节创建控制器
    fk_ctrls = []
    for jnt in jnts_fk_new:
        jntCtrl = cmds.duplicate(ctrl, name=jnt.replace('jnt_', 'ctrl_'))[0]
        cmds.matchTransform(jntCtrl, jnt, position=True, rotation=True)
        jntCtrl_name = jnt.replace('jnt_', 'ctrl_')
        cmds.rename(jntCtrl, jntCtrl_name)
        jntCtrl = [jntCtrl]
        ctrl_hirachy(jntCtrl)
        fk_ctrls.append(jntCtrl[0])

    for ctrl, jnt in zip(fk_ctrls, jnts_fk_new):
        jnt_parent = cmds.listRelatives(jnt, parent=True)[0]
        if jnt_parent:
            ctrl_parent = jnt_parent.replace("jnt_", "ctrl_")
            zero_ctrl = jnt.replace("jnt_", "zero_")
            if jnt_parent in jnts_fk_new:
                cmds.parent(zero_ctrl, ctrl_parent)
                constraint_node = cmds.parentConstraint(ctrl, jnt, maintainOffset=False)[0]
                cmds.setAttr(constraint_node + ".interpType", 2)

    # 创建FKIK切换用的控制器
    switcher = cmds.curve(d=1, p=[(-1, 0, 1), (-1, 0, 3), (1, 0, 3), (1, 0, 1), (3, 0, 1), (3, 0, -1),
                                  (1, 0, -1), (1, 0, -3), (-1, 0, -3), (-1, 0, -1), (-3, 0, -1), (-3, 0, 1),
                                  (-1, 0, 1)], k=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
    nameParts = jnt_thigh.split("_")
    name_switcher = "ctrl_{}_{}FKIKSwitch_{}".format(nameParts[1], nameParts[2], nameParts[3])
    cmds.rename(switcher, name_switcher)
    ctrl_hirachy([name_switcher])

    # 把switcher的zero组放到对应的位置
    zero_switcher = name_switcher.replace("ctrl_", "zero_")
    cmds.matchTransform(zero_switcher, jnt_thigh, position=True, rotation=True)
    cmds.move(-40, 0, 0, zero_switcher, relative=True, objectSpace=False)

    # 添加fkik切换的属性，并把其他无关的属性锁定并隐藏
    switcher = name_switcher
    for attr in ["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz", "ro"]:
        cmds.setAttr(switcher + ".{}".format(attr), lock=True, keyable=False, channelBox=False)

    cmds.addAttr(switcher, ln="FKIKSwitch", at="double", min=0, max=1, dv=0)
    cmds.setAttr("{}.FKIKSwitch".format(switcher), e=True, keyable=True)

    # 首先连接FKIK控制器的可见性切换，0是FK，1是IK
    zero_FKs = fk_ctrls[0].replace("ctrl_", "zero_")
    zero_pole = zero_loc

    # 先连接IK组
    for zero in [zero_pole, zeroGrp_ctrl_ik]:
        cmds.connectAttr("{}.FKIKSwitch".format(switcher), "{}.visibility".format(zero))

    # 然后是FK控制器最上层组
    rvs = cmds.createNode("reverse")
    cmds.connectAttr("{}.FKIKSwitch".format(switcher), rvs + ".inputX")
    cmds.connectAttr(rvs + ".outputX", zero_FKs + ".visibility")

    # 接着创建FKIKjnts对原始权重关节的约束，这里不能用常规的连接，因为fkik关节是放在一个世界空间的组下的，连接的数值不对，尝试在这里用矩阵约束
    # 这里的矩阵约束不用做保持偏移选项，暂时也不用做缩放方面的约束
    # 首先要获得三组关节的列表
    for jnt_src, jnt_ik, jnt_fk in zip(jnts_src, jnts_ik_new, jnts_fk_new):
        # 直接用jnt_ik和jnt_fk作为父级去直接约束jnt_src
        constraint_node = cmds.parentConstraint([jnt_fk, jnt_ik], jnt_src, maintainOffset=True)[0]
        cmds.setAttr(constraint_node + ".interpType", 2)
        cmds.connectAttr("{}.FKIKSwitch".format(switcher), constraint_node + ".{}W1".format(jnt_ik))
        cmds.connectAttr(rvs + ".outputX", constraint_node + ".{}W0".format(jnt_fk))

    # 整理
    cmds.delete(fk_ctrl_shape)

    # 创建一个放控制器的组
    grp_ctrls = cmds.createNode("transform", n="grp_{}_{}Ctrls_{}".format(nameParts[1], nameParts[2], nameParts[3]))
    cmds.parent(zero_loc, grp_ctrls)
    cmds.parent(zero_FKs, grp_ctrls)
    cmds.parent(zeroGrp_ctrl_ik, grp_ctrls)
    cmds.parent(zero_switcher, grp_ctrls)
    zero_fk_ctrlEnd = fk_ctrls[-1].replace("ctrl_", "zero_")
    cmds.delete(zero_fk_ctrlEnd)


def run_human_leg_rig():
    """选中人腿最上层关节，自动生成 IK/FK 控制器、极向量及 FKIK 切换系统。"""
    sel = cmds.ls(selection=True)
    if not sel:
        cmds.warning("请先选择腿最上层的关节。")
        return
    human_leg(sel[0])
# 给马创建腿部的IK和FK控制器，控制器形状统一用圆圈，可以用adv的swap curve功能来替换控制器形状
# 首先场景里已经有了命好名的马腿关节（前后），注意springIK需要至少3跟骨骼（4个关节）才有效
# 创建一个函数，函数会遍历选择的关节的所有子关节，然后会复制出新的IK关节链并对选择的关节和倒数第二节子关节做springIK，
# 并对倒数第二节关节和倒数第一节关节做一个scik
# ik关节链会被放在一个位于世界坐标原点的组里，然后用原关节链的父级关节约束ik关节链的最上层关节
# 再复制一个FK关节链
# 接着是控制器的创建与约束

# 函数的创建
def ctrl_hirachy(ctrls):
    CTRL_COLORS = {'m': 17,
                   'l': 6,
                   'r': 13}

    SUB_COLORS = {'m': 25,
                  'l': 15,
                  'r': 4}

    # loop in each ctrl and create hierarchy
    for ctrl in ctrls:
        # get name parts
        name_parts = ctrl.split('_')

        # create zero group
        zero = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'zero_'))
        # create driven group
        driven = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'driven_'), parent=zero)
        # create connect group
        connect = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'connect_'), parent=driven)
        # create offset group
        offset = cmds.createNode('transform', name=ctrl.replace('ctrl_', 'offset_'), parent=connect)

        # snap to control position
        cmds.matchTransform(zero, ctrl, position=True, rotation=True)

        # parent control to offset group
        cmds.parent(ctrl, offset)

        # freeze transformation for controller
        cmds.makeIdentity(ctrl, apply=True, scale=True)
        # delete history
        cmds.select(ctrl)
        cmds.DeleteHistory()

        # show rotate order
        cmds.setAttr(ctrl + '.rotateOrder', channelBox=True)

        # lock hide unused attrs
        for attr in ['visibility']:
            for ctrl_node in [ctrl]:
                cmds.setAttr('{}.{}'.format(ctrl_node, attr), keyable=False, channelBox=False, lock=True)

        # set color
        for ctrl_node, col_idx in zip([ctrl], [CTRL_COLORS[name_parts[1]]]):
            # get shape node
            shape_node = cmds.listRelatives(ctrl_node, shapes=True)[0]
            # set color
            cmds.setAttr(shape_node + '.overrideEnabled', 1)
            cmds.setAttr(shape_node + '.overrideColor', col_idx)

def horse_leg(jnt_thigh):
    """

    :param jnt_thigh: str, 马腿最上层的关节
    :return: none
    """

    # 获取jnt_thigh的父关节用于后续约束spik关节
    jnt_thigh_parent = cmds.listRelatives(jnt_thigh, parent=True)

    # 遍历jnt_thigh的子关节，把他们都装进一个列表
    jnts_src = cmds.listRelatives(jnt_thigh, ad=True, type="joint")
    jnts_src.reverse()
    jnts_src.insert(0,jnt_thigh)

    # 复制原始关节链并给新的关节description里加上spik
    jnts_spik = cmds.duplicate(jnt_thigh, renameChildren=True)

    # 创建一个空列表用于存放改好名字的spik关节
    jnts_spik_new = []

    for jnt_src, jnt_spik in zip(jnts_src, jnts_spik):
        nameParts = jnt_src.split('_')
        name_description_spik = nameParts[2] + "Spik"
        name_jnt_spik = "{}_{}_{}_{}".format(nameParts[0], nameParts[1], name_description_spik, nameParts[3])
        cmds.rename(jnt_spik, name_jnt_spik)
        jnts_spik_new.append(name_jnt_spik)

    # 把spik关节链放入一个位于世界坐标原点的组
    grp_name = jnts_spik_new[0].replace("jnt_", "jntGrp_")
    grp_spik = cmds.createNode('transform', name=grp_name)
    cmds.parent(jnts_spik_new[0], grp_spik)
    cmds.setAttr(grp_spik + ".visibility", 0)

    # 创建spik
    spikHandle = cmds.ikHandle(sj=jnts_spik_new[0], ee=jnts_spik_new[-2], sol="ikSpringSolver")[0]

    # 创建控制器形状
    ctrl_name_spik = grp_name.replace("jntGrp_", "ctrl_")
    ctrl_spik = cmds.curve(d=1, p=[(-0.5, 0.5, 0.5), (0.5, 0.5, 0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5),
                                   (-0.5, 0.5, 0.5),(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5),
                                   (0.5, -0.5, 0.5), (0.5, -0.5, -0.5),(0.5, 0.5, -0.5), (0.5, -0.5, -0.5),
                                   (-0.5, -0.5, -0.5), (-0.5, 0.5, -0.5), (-0.5, -0.5, -0.5),(-0.5, -0.5, 0.5)],
                                    k=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],n=ctrl_name_spik)
    ctrl_hirachy([ctrl_spik])
    zeroGrp_ctrl_spik = ctrl_spik.replace("ctrl_", "zero_")
    cmds.matchTransform(zeroGrp_ctrl_spik, jnts_spik_new[-2], position=True, rotation=True)
    ctrl_shape = cmds.listRelatives(ctrl_spik, shapes=True)[0]
    cmds.select(ctrl_shape + ".cv[0]", ctrl_shape + ".cv[1]", ctrl_shape + ".cv[2]", ctrl_shape + ".cv[3]",
                ctrl_shape + ".cv[4]", ctrl_shape + ".cv[5]", ctrl_shape + ".cv[6]", ctrl_shape + ".cv[7]",
                ctrl_shape + ".cv[8]", ctrl_shape + ".cv[9]", ctrl_shape + ".cv[10]", ctrl_shape + ".cv[11]",
                ctrl_shape + ".cv[12]", ctrl_shape + ".cv[13]", ctrl_shape + ".cv[14]", ctrl_shape + ".cv[15]", )
    cmds.scale(16, 16, 16, r=True, ocp=True)

    # 把ik控制柄p给控制器并隐藏
    cmds.parent(spikHandle, ctrl_spik)
    cmds.setAttr(spikHandle + ".visibility", 0)

    # 创建一个loc作为极向量
    name_parts_new = jnts_spik_new[0].split('_')
    description_new = name_parts_new[2] + "Pole"
    loc_spik_name = "ctrl_{}_{}_{}".format(name_parts_new[1], description_new, name_parts_new[3])
    loc_spik = cmds.spaceLocator(name=loc_spik_name)[0]
    ctrl_hirachy([loc_spik])

    # 把极向量控制器和倒数第三个关节匹配位置
    zero_loc = loc_spik.replace("ctrl_", "zero_")
    cmds.parent(zero_loc, jnts_spik_new[-3])
    for attr in [".tx", ".ty", ".tz", ".rx", ".ry", ".rz"]:
        cmds.setAttr(zero_loc + attr, 0)

    # 把极向量的zero组沿当前父对象的y轴移动60个单位, 并p出关节
    if "_r_" in jnt_thigh:
        cmds.setAttr(zero_loc + ".ty", 60)
        cmds.parent(zero_loc, world=True)
    elif "_l_" in jnt_thigh:
        cmds.setAttr(zero_loc + ".ty", -60)
        cmds.parent(zero_loc, world=True)

    # 放大loc
    for attr in ["Shape.localScaleX", "Shape.localScaleY", "Shape.localScaleZ"]:
        cmds.setAttr(loc_spik + attr, 15)

    # 创建极向量约束
    cmds.poleVectorConstraint(loc_spik, spikHandle, weight=1)

    # 给末端一节骨骼创建scik
    scikHandle = cmds.ikHandle(sj=jnts_spik_new[-2], ee=jnts_spik_new[-1], sol="ikSCsolver")[0]
    cmds.parent(scikHandle, ctrl_spik)
    cmds.setAttr(scikHandle + ".visibility", 0)

    # 用jnt_thigh_parent约束spik关节
    if jnt_thigh_parent:
        constraint_node = cmds.parentConstraint(jnt_thigh_parent, jnts_spik_new[0], maintainOffset=True)[0]
        cmds.setAttr(constraint_node + ".interpType", 2)


    # IK控制系统已经设置完毕， 接下来是FK控制系统以及ikfk切换系统
    # 首先是FK系统
    # 复制原始关节链并给新的关节description里加上Fk
    jnts_fk = cmds.duplicate(jnt_thigh, renameChildren=True)

    # 创建一个空列表用于存放改好名字的fk关节
    jnts_fk_new = []

    for jnt_src, jnt_fk in zip(jnts_src, jnts_fk):
        nameParts = jnt_src.split('_')
        name_description_fk = nameParts[2] + "Fk"
        name_jnt_fk = "{}_{}_{}_{}".format(nameParts[0], nameParts[1], name_description_fk, nameParts[3])
        cmds.rename(jnt_fk, name_jnt_fk)
        jnts_fk_new.append(name_jnt_fk)

    # 把fk关节链也放到spik关节链的组下
    cmds.parent(jnts_fk_new[0], grp_spik)

    # 创建圆环控制器
    ctrl = cmds.circle( nr=(1, 0, 0), c=(0, 0, 0) )[0]
    fk_ctrl_shape = ctrl
    cmds.select(ctrl + ".cv[0]", ctrl + ".cv[1]", ctrl + ".cv[2]", ctrl + ".cv[3]", ctrl + ".cv[4]", ctrl + ".cv[5]",
                ctrl + ".cv[6]", ctrl + ".cv[7]")
    cmds.scale(9, 9, 9, r=True, ocp=True)

    # 给每个fk关节创建控制器
    fk_ctrls = []
    for jnt in jnts_fk_new:
        jntCtrl = cmds.duplicate(ctrl, name=jnt.replace('jnt_', 'ctrl_'))[0]
        cmds.matchTransform(jntCtrl, jnt, position=True, rotation=True)
        jntCtrl_name = jnt.replace('jnt_', 'ctrl_')
        cmds.rename(jntCtrl, jntCtrl_name)
        jntCtrl = [jntCtrl]
        ctrl_hirachy(jntCtrl)
        fk_ctrls.append(jntCtrl[0])

    for ctrl, jnt in zip(fk_ctrls, jnts_fk_new):
        jnt_parent = cmds.listRelatives(jnt, parent=True)[0]
        if jnt_parent:
            ctrl_parent = jnt_parent.replace("jnt_", "ctrl_")
            zero_ctrl = jnt.replace("jnt_", "zero_")
            if jnt_parent in jnts_fk_new:
                cmds.parent(zero_ctrl, ctrl_parent)
                constraint_node = cmds.parentConstraint(ctrl, jnt, maintainOffset=False)[0]
                cmds.setAttr(constraint_node + ".interpType", 2)

    # 创建FKIK切换用的控制器
    switcher = cmds.curve(d=1, p=[(-1, 0, 1), (-1, 0, 3), (1, 0, 3), (1, 0, 1), (3, 0, 1), (3, 0, -1),
                                  (1, 0, -1), (1, 0, -3), (-1, 0, -3), (-1, 0, -1), (-3, 0, -1), (-3, 0, 1),
                                  (-1, 0, 1)], k=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
    nameParts = jnt_thigh.split("_")
    name_switcher = "ctrl_{}_{}FKIKSwitch_{}".format(nameParts[1], nameParts[2], nameParts[3])
    cmds.rename(switcher, name_switcher)
    ctrl_hirachy([name_switcher])

    # 把switcher的zero组放到对应的位置
    zero_switcher = name_switcher.replace("ctrl_", "zero_")
    cmds.matchTransform(zero_switcher, jnt_thigh, position=True, rotation=True)
    cmds.move(-40, 0, 0, zero_switcher, relative=True, objectSpace=False)

    # 添加fkik切换的属性，并把其他无关的属性锁定并隐藏
    switcher = name_switcher
    for attr in ["tx", "ty", "tz", "rx", "ry", "rz", "sx", "sy", "sz", "ro"]:
        cmds.setAttr(switcher + ".{}".format(attr), lock=True, keyable=False, channelBox=False)

    cmds.addAttr(switcher, ln="FKIKSwitch", at="double", min=0, max=1, dv=0)
    cmds.setAttr("{}.FKIKSwitch".format(switcher), e=True, keyable=True)

    # 首先连接FKIK控制器的可见性切换，0是FK，1是IK
    zero_FKs = fk_ctrls[0].replace("ctrl_", "zero_")
    zero_pole = zero_loc

    # 先连接IK组
    for zero in [zero_pole, zeroGrp_ctrl_spik]:
        cmds.connectAttr("{}.FKIKSwitch".format(switcher), "{}.visibility".format(zero))

    # 然后是FK控制器最上层组
    rvs = cmds.createNode("reverse")
    cmds.connectAttr("{}.FKIKSwitch".format(switcher), rvs + ".inputX")
    cmds.connectAttr(rvs + ".outputX", zero_FKs + ".visibility")

    # 接着创建FKIKjnts对原始权重关节的约束，这里不能用常规的连接，因为fkik关节是放在一个世界空间的组下的，连接的数值不对，尝试在这里用矩阵约束
    # 这里的矩阵约束不用做保持偏移选项，暂时也不用做缩放方面的约束
    # 首先要获得三组关节的列表
    for jnt_src, jnt_spik, jnt_fk in zip(jnts_src, jnts_spik_new, jnts_fk_new):
        # 先创建ik关节到src关节的矩阵连接
        multMatrix_ik = cmds.createNode("multMatrix")
        decomposeMatrix_ik = cmds.createNode("decomposeMatrix")
        cmds.connectAttr(jnt_spik + ".worldMatrix[0]", multMatrix_ik + ".matrixIn[0]")
        cmds.connectAttr(jnt_src + ".parentInverseMatrix[0]",
                         multMatrix_ik + ".matrixIn[1]")
        cmds.connectAttr(multMatrix_ik + ".matrixSum", decomposeMatrix_ik + ".inputMatrix")

        # 这里就可以从decomposeMatrix_ik里获取到.outputRotate和.outputTranslate,先创建一个融合权重的节点
        FKIKBlend_rotate = cmds.createNode("blendColors")
        FKIKBlend_translate = cmds.createNode("blendColors")

        # 创建fk关节的矩阵链接
        multMatrix_fk = cmds.createNode("multMatrix")
        decomposeMatrix_fk = cmds.createNode("decomposeMatrix")
        cmds.connectAttr(jnt_fk + ".worldMatrix[0]", multMatrix_fk + ".matrixIn[0]")
        cmds.connectAttr(jnt_src + ".parentInverseMatrix[0]",
                         multMatrix_fk + ".matrixIn[1]")
        cmds.connectAttr(multMatrix_fk + ".matrixSum", decomposeMatrix_fk + ".inputMatrix")

        # 连接fkik的旋转位移值到blend节点
        # rotate
        cmds.connectAttr(decomposeMatrix_fk + ".outputRotate", FKIKBlend_rotate + ".color1")
        cmds.connectAttr(decomposeMatrix_ik + ".outputRotate", FKIKBlend_rotate + ".color2")
        cmds.connectAttr(FKIKBlend_rotate + ".output", jnt_src + ".rotate")
        cmds.connectAttr(rvs + ".outputX", FKIKBlend_rotate + ".blender")

        # tranlate
        cmds.connectAttr(decomposeMatrix_fk + ".outputTranslate", FKIKBlend_translate + ".color1")
        cmds.connectAttr(decomposeMatrix_ik + ".outputTranslate", FKIKBlend_translate + ".color2")
        cmds.connectAttr(FKIKBlend_translate + ".output", jnt_src + ".translate")
        cmds.connectAttr(rvs + ".outputX", FKIKBlend_translate + ".blender")

    # 整理
    cmds.delete(fk_ctrl_shape)

    # 创建一个放控制器的组
    grp_ctrls = cmds.createNode("transform", n="grp_{}_{}Ctrls_{}".format(nameParts[1], nameParts[2], nameParts[3]))
    cmds.parent(zero_loc, grp_ctrls)
    cmds.parent(zero_FKs, grp_ctrls)
    cmds.parent(zeroGrp_ctrl_spik, grp_ctrls)
    cmds.parent(zero_switcher, grp_ctrls)
    zero_fk_ctrlEnd = fk_ctrls[-1].replace("ctrl_", "zero_")
    cmds.delete(zero_fk_ctrlEnd)


def run_horse_leg_rig():
    """选中马腿最上层关节，自动生成 springIK + SC IK 与 FK 控制器及 FKIK 切换系统。"""
    sel = cmds.ls(selection=True)
    if not sel:
        cmds.warning("请先选择马腿最上层的关节。")
        return
    horse_leg(sel[0])
def run_tentacle_ripple(ctrl=None):
    """给触须驱动控制器添加涟漪缩放属性（依赖 MASH 插件）。

    :param ctrl: (str/None) 驱动控制器名，默认 ctrl_m_tentacleDrv_001。
    """
    if ctrl is None:
        result = prompt_fields(
            "触须涟漪缩放",
            [("驱动控制器名", "ctrl_m_tentacleDrv_001")])
        if result is None:
            return
        ctrl = result[0].strip() or 'ctrl_m_tentacleDrv_001'

    if not cmds.objExists(ctrl):
        cmds.warning("控制器 {} 不存在。".format(ctrl))
        return

    # add attrs
    cmds.addAttr(ctrl, longName='ripple', attributeType='float', keyable=True)
    cmds.addAttr(ctrl, longName='rippleOut', attributeType='float', keyable=False)
    cmds.addAttr(ctrl, longName='rippleFrequency', attributeType='float', keyable=True, minValue=0, defaultValue=5)
    cmds.addAttr(ctrl, longName='rippleAmplitude', attributeType='float', keyable=True, minValue=1, defaultValue=1.5)
    cmds.addAttr(ctrl, longName='rippleOffset', attributeType='float', keyable=True)
    cmds.addAttr(ctrl, longName='rippleFalloff', attributeType='float', keyable=True, minValue=0, maxValue=1,
                 defaultValue=0.05)

    # create modulo expression to connect ripple out
    cmds.expression(string='{}.rippleOut = ({}.ripple + {}.rippleOffset) % {}.rippleFrequency'.format(ctrl, ctrl, ctrl,
                                                                                                      ctrl),
                    name='expr_m_rippleOut_001')

    ripple_attr = ctrl + '.rippleOut'
    freq_attr = ctrl + '.rippleFrequency'
    amp_attr = ctrl + '.rippleAmplitude'
    offset_attr = ctrl + '.rippleOffset'
    fall_attr = ctrl + '.rippleFalloff'

    # remap ripple out to 0-1
    remap = cmds.createNode('remapValue', name='remap_m_tentacleRippleVal_001')
    cmds.connectAttr(ripple_attr, remap + '.inputValue')
    cmds.connectAttr(freq_attr, remap + '.inputMax')

    ripple_attr = remap + '.outValue'

    # falloff attr reverse
    mult_rvs = cmds.createNode('multDoubleLinear', name='mult_m_tentacleRippleNeg_001')
    cmds.connectAttr(falloff_attr, mult_rvs + '.input1')
    cmds.setAttr(mult_rvs + '.input2', -1)
    falloff_rvs_attr = mult_rvs + '.output'

    # get joints and joints number
    jnts = cmds.ls('jnt_m_tentacle_???')[:-1]
    jnts_num = len(jnts)

    # create MASH Distribute node
    distr = cmds.createNode('MASH_Distribute', name='distribute_m_tentacleRipple_001')
    cmds.setAttr(distr + '.pointCount', jnts_num)

    # connect scale with amplitude
    cmds.connectAttr(amp_attr, distr + '.scaleY')
    cmds.connectAttr(amp_attr, distr + '.scaleZ')

    # create breakout node
    breakout = cmds.createNode('MASH_Breakout', name='breakout_m_tentacleRipple_001')
    cmds.connectAttr(distr + '.outputPoints', breakout + '.inputPoints')

    # loop in each joint
    unit_val = 1 / float(jnts_num + 1)
    for i, j in enumerate(jnts):
        # create remap node to do wave effect
        remap_jnt = cmds.createNode('remapValue', name='remap_m_tentacleRipple_{:03d}'.format(i + 1))

        # connect ripple value to input value
        cmds.connectAttr(ripple_attr, remap_jnt + '.inputValue')

        # set ramp position
        cmds.setAttr(remap_jnt + '.value[1].value_Position', (i + 0.5) * unit_val)
        cmds.setAttr(remap_jnt + '.value[1].value_FloatValue', 1)
        cmds.setAttr(remap_jnt + '.value[1].value_Interp', 2)

        # set in and out point, default should be half unit
        add_in = cmds.createNode('addDoubleLinear', name='add_m_tentacleRippleIn_{:03d}'.format(i + 1))
        add_out = cmds.createNode('addDoubleLinear', name='add_m_tentacleRippleOut_{:03d}'.format(i + 1))

        cmds.setAttr(add_in + '.input1', i * unit_val)
        cmds.setAttr(add_out + '.input1', (i + 1) * unit_val)

        cmds.connectAttr(falloff_rvs_attr, add_in + '.input2')
        cmds.connectAttr(fall_attr, add_out + '.input2')

        # clamp the output in 0-1
        clamp = cmds.createNode('clamp', name='clamp_m_tentacleRipple_{:03d}'.format(i + 1))
        cmds.connectAttr(add_in + '.output', clamp + '.inputR')
        cmds.connectAttr(add_out + '.output', clamp + '.inputG')
        cmds.setAttr(clamp + '.maxR', 1)
        cmds.setAttr(clamp + '.maxG', 1)

        # connect clamp output to point position
        cmds.connectAttr(clamp + '.outputR', remap_jnt + '.value[0].value_Position')
        cmds.setAttr(remap_jnt + '.value[0].value_FloatValue', 0)
        cmds.setAttr(remap_jnt + '.value[0].value_Interp', 2)
        cmds.connectAttr(clamp + '.outputG', remap_jnt + '.value[2].value_Position')
        cmds.setAttr(remap_jnt + '.value[2].value_FloatValue', 0)
        cmds.setAttr(remap_jnt + '.value[2].value_Interp', 2)

        # connect output ripple wight with MASH Distribute node
        blend_ripple = cmds.createNode('blendColors', name='blend_m_tentacleRippleScale_{:03d}'.format(i + 1))
        cmds.connectAttr(remap_jnt + '.outValue', blend_ripple + '.blender')
        cmds.connectAttr('{}.outputs[{}].scale'.format(breakout, i), blend_ripple + '.color1')
        cmds.setAttr(blend_ripple + '.color2', 1, 1, 1)

        # connect to joint's scale
        cmds.connectAttr(blend_ripple + '.outputG', j + '.scaleY')
        cmds.connectAttr(blend_ripple + '.outputB', j + '.scaleZ')
def run_tentacle_roll(ctrl=None):
    """给触须控制器添加卷曲属性（依赖 MASH 插件）。

    :param ctrl: (str/None) 驱动控制器名，默认 ctrl_m_tentacleDrv_001。
    """
    if ctrl is None:
        result = prompt_fields(
            "触须卷曲",
            [("驱动控制器名", "ctrl_m_tentacleDrv_001")])
        if result is None:
            return
        ctrl = result[0].strip() or 'ctrl_m_tentacleDrv_001'

    if not cmds.objExists(ctrl):
        cmds.warning("控制器 {} 不存在。".format(ctrl))
        return

    # add attrs
    cmds.addAttr(ctrl, longName='roll', attributeType='float', minValue=0, maxValue=1, keyable=True)
    cmds.addAttr(ctrl, longName='angle', attributeType='float', keyable=True, defaultValue=70)
    cmds.addAttr(ctrl, longName='falloff', attributeType='float', keyable=True, minValue=0, maxValue=1,
                 defaultValue=0.1)

    roll_attr = ctrl + '.roll'
    angle_attr = ctrl + '.angle'
    falloff_attr = ctrl + '.falloff'

    # mult node to reverse falloff value, because we are using this value to subtract
    mult = cmds.createNode('multDoubleLinear', name='mult_m_tentacleRollFalloffRvs_001')
    cmds.connectAttr(falloff_attr, mult + '.input1')
    cmds.setAttr(mult + '.input2', -1)
    falloff_attr = mult + '.output'

    # list controls
    ctrls = cmds.ls('ctrl_m_tentacle_???', type='transform')

    # get control number
    ctrl_num = len(ctrls)

    # create mash distribute node
    distr = cmds.createNode('MASH_Distribute', name='distribute_m_tentacleRoll_001')
    cmds.setAttr(distr + '.pointCount', ctrl_num)

    # set rotate X to 1 to gather weight value from 0-1
    cmds.setAttr(distr + '.rotateX', 1)

    # create breakout node
    breakout = cmds.createNode('MASH_Breakout', name='breakout_m_tentacleRoll_001')
    cmds.connectAttr(distr + '.outputPoints', breakout + '.inputPoints')

    # loop in each control to do the roll setup
    for i, ctrl in enumerate(ctrls):
        # get connect group
        connect = ctrl.replace('ctrl_', 'connect_')

        # create remap node to roll will only happen in the given section
        remap = cmds.createNode('remapValue', name='remap_m_tentacleRollWeight_{:03d}'.format(i + 1))

        # connect tentacle roll to remap
        cmds.connectAttr(roll_attr, remap + '.inputValue')
        # get max value by weight
        weight_max = 1 - float(i) / ctrl_num
        cmds.setAttr(remap + '.inputMax', weight_max)
        # get min value
        weight_min = 1 - float(i + 1) / ctrl_num
        # add node to subtract falloff so the joint can roll before the previous finished
        add = cmds.createNode('addDoubleLinear', name='add_m_tentacleRollStart_{:03d}'.format(i + 1))
        cmds.setAttr(add + '.input1', weight_min)
        cmds.connectAttr(falloff_attr, add + '.input2')
        # clamp value so it won't go below 0
        clamp = cmds.createNode('clamp', name='clamp_m_tentacleRollStart_{:03d}'.format(i + 1))
        cmds.setAttr(clamp + '.maxR', 1)
        cmds.connectAttr(add + '.output', clamp + '.inputR')
        # connect with min value
        cmds.connectAttr(clamp + '.outputR', remap + '.inputMin')

        # multiply divide node to mult remap weight with distribute weight to get the final roll weight for each joint
        # because MASH doesn't work with single axis, we need to use multiply divide to breakout single axis rotation
        mult_weight = cmds.createNode('multiplyDivide', name='mult_m_tentacleRotWeight_{:03d}'.format(i + 1))
        cmds.connectAttr(remap + '.outValue', mult_weight + '.input1X')
        cmds.connectAttr('{}.outputs[{}].rotate'.format(breakout, i), mult_weight + '.input2')

        # mult with roll angle to get output
        mult_angle = cmds.createNode('multDoubleLinear', name='mult_m_tentacleRotAngle_{:03d}'.format(i + 1))
        cmds.connectAttr(mult_weight + '.outputX', mult_angle + '.input1')
        cmds.connectAttr(angle_attr, mult_angle + '.input2')

        # connect with connect group
        cmds.connectAttr(mult_angle + '.output', connect + '.rotateZ')
def run_anim_path_locators():
    """选中的物体在每一帧的位置生成一个 locator，归入 Locator_Group 组（粉色）。"""
    selected_objects = cmds.ls(selection=True)

    if not selected_objects:
        cmds.warning("请先选择一个物体")
        return

    start_frame = cmds.playbackOptions(query=True, minTime=True)
    end_frame = cmds.playbackOptions(query=True, maxTime=True)

    group_name = cmds.group(empty=True, name="Locator_Group")

    for frame in range(int(start_frame), int(end_frame) + 1):
        cmds.currentTime(frame)
        for obj in selected_objects:
            position = cmds.xform(obj, query=True, translation=True, worldSpace=True)
            locator = cmds.spaceLocator(name="locator_{}_{}".format(obj, frame))[0]
            cmds.xform(locator, worldSpace=True, translation=position)

            cmds.parent(locator, group_name)

            cmds.setAttr(locator + '.localScaleX', 0.1)
            cmds.setAttr(locator + '.localScaleY', 0.1)
            cmds.setAttr(locator + '.localScaleZ', 0.1)
            cmds.setAttr(locator + ".overrideEnabled", 1)
            cmds.setAttr(locator + ".overrideColor", 20)

    cmds.confirmDialog(title="完成", message="所有locator已创建并归入组中，颜色已设置为粉色！", button=["确定"])
def get_attr_keyframes(full_attr):
    """获取 full_attr 被 k 了帧的关键帧，full_attr 格式为 obj.attr。

    :param full_attr: (str)
    :return: (list) keyframes
    """
    frames_attr = []
    start_frame = int(cmds.playbackOptions(query=True, minTime=True))
    end_frame = int(cmds.playbackOptions(query=True, maxTime=True))

    print("检查 '{}' 的关键帧：".format(full_attr))
    keyframes = cmds.keyframe(full_attr, query=True, timeChange=True)

    for frame in range(start_frame, end_frame + 1):
        if frame in keyframes:
            frames_attr.append(frame)

    return frames_attr


def anim_to_SDK(drivens, driven_attrs, driver_attr, mirror=True):
    """把已 k 好的动画转成 SDK（setDrivenKey）驱动，并给每个被驱动控制器添加 autoDrive 开关。

    :param drivens: (list) 被驱动控制器。
    :param driven_attrs: (list) 被驱动属性（只能是位移/旋转/缩放）。
    :param driver_attr: (str) 驱动属性。
    :param mirror: (bool) 是否镜像。
    """
    # 获取driver_attr_mirror
    if '_L' in driver_attr:
        driver_attr_mirror = driver_attr.replace('_L', '_R')
    if '_R' in driver_attr:
        driver_attr_mirror = driver_attr.replace('_R', '_L')
    if '_l_' in driver_attr:
        driver_attr_mirror = driver_attr.replace('_l_', '_r_')
    if '_r_' in driver_attr:
        driver_attr_mirror = driver_attr.replace('_r_', '_l_')

    for driven in drivens:
        name_parts = driven.split('_')
        if len(name_parts) == 2:
            name_des = name_parts[0]
            name_side = name_parts[1]
            name_side_mirror = []
            if name_side == 'R':
                name_side_mirror = 'L'
            if name_side == 'L':
                name_side_mirror = 'R'
            name_fik = name_des[:2]
            name_description = name_des[2:]
            driven_mirror = '{}_{}'.format(name_des, name_side_mirror)
            name_sdk = '{}Sdk{}_{}'.format(name_fik, name_description, name_side)
            name_sdk_mirror = '{}Sdk{}_{}'.format(name_fik, name_description, name_side_mirror)

        else:
            name_side = name_parts[1]
            name_description = name_parts[2]
            name_side_mirror = []
            if name_side == 'l':
                name_side_mirror = 'r'
            if name_side == 'r':
                name_side_mirror = 'l'

            driven_mirror = '{}_{}_{}_{}'.format(name_parts[0], name_side_mirror, name_parts[2], name_parts[3])
            name_sdk = 'sdk_{}_{}_{}'.format(name_parts[1], name_parts[2], name_parts[3])
            name_sdk_mirror = 'sdk_{}_{}_{}'.format(name_side_mirror, name_parts[2], name_parts[3])

        # 查询sdk组是否已经存在, 不存在就在控制器的父层级上创建sdk组
        cmds.currentTime(1)
        if not cmds.objExists(name_sdk):
            sdk_grp = cmds.createNode('transform', name=name_sdk)
            cmds.matchTransform(sdk_grp, driven)
            parent_grp = cmds.listRelatives(driven, parent=True)[0]
            pp_grp = cmds.listRelatives(parent_grp, parent=True)[0]
            cmds.parent(parent_grp, sdk_grp)
            cmds.parent(sdk_grp, pp_grp)

            if mirror:
                sdk_grp_mirror = cmds.createNode('transform', name=name_sdk_mirror)
                cmds.matchTransform(sdk_grp_mirror, driven_mirror)
                parent_grp_mirror = cmds.listRelatives(driven_mirror, parent=True)[0]
                pp_grp_mirror = cmds.listRelatives(parent_grp_mirror, parent=True)[0]
                cmds.parent(parent_grp_mirror, sdk_grp_mirror)
                cmds.parent(sdk_grp_mirror, pp_grp_mirror)

        else:
            sdk_grp = name_sdk
            sdk_grp_mirror = name_sdk_mirror

        # 给控制器添加开关属性
        if not cmds.objExists(driven + '.autoDrive'):
            cmds.addAttr(driven, longName='autoDrive', attributeType='double', min=0, max=1, dv=1)
            cmds.setAttr(driven + '.autoDrive', keyable=True)
            if mirror:
                cmds.addAttr(driven_mirror, longName='autoDrive', attributeType='double', min=0, max=1, dv=1)
                cmds.setAttr(driven_mirror + '.autoDrive', keyable=True)

        for attr in driven_attrs:
            driven_switch = cmds.createNode('multDoubleLinear', name='mult_{}_{}{}Switch_001'.format(name_side.lower(),
                                            name_description, attr.capitalize()))
            cmds.connectAttr(driven + '.autoDrive', driven_switch + '.input1')
            driven_attr_name = driven + '.' + attr
            keyframes_attr = get_attr_keyframes(driven_attr_name)
            sdk_attr_name = sdk_grp + '.' + attr
            if mirror:
                driven_attr_name_mirror = driven_mirror + '.' + attr
                sdk_attr_name_mirror = sdk_grp_mirror + '.' + attr
                driven_switch_mirror = cmds.createNode('multDoubleLinear',
                                                       name='mult_{}_{}{}Switch_001'.format(name_side_mirror.lower(),
                                                       name_description, attr.capitalize()))
                cmds.connectAttr(driven_mirror + '.autoDrive', driven_switch_mirror + '.input1')

            for frame in keyframes_attr:
                cmds.currentTime(frame)
                driver_attr_val = cmds.getAttr(driver_attr)
                driven_attr_val = cmds.getAttr(driven_attr_name)
                cmds.setDrivenKeyframe(sdk_attr_name, currentDriver=driver_attr,
                                       driverValue=driver_attr_val, value=driven_attr_val)

                if mirror:
                    driver_attr_val_mirror = driver_attr_val
                    if attr in ['tx', 'ty', 'tz']:
                        driven_attr_val_mirror = -driven_attr_val

                    if attr in ['rx', 'ry', 'rz']:
                        driven_attr_val_mirror = driven_attr_val

                    if attr in ['sx', 'sy', 'sz']:
                        driven_attr_val_mirror = driven_attr_val

                    cmds.setDrivenKeyframe(sdk_attr_name_mirror, currentDriver=driver_attr_mirror,
                                           driverValue=driver_attr_val_mirror, value=driven_attr_val_mirror)

            sdkNode_output = cmds.listConnections(sdk_attr_name, plugs=True, s=True, d=False)[0]
            cmds.disconnectAttr(sdkNode_output, sdk_attr_name)
            cmds.connectAttr(sdkNode_output, driven_switch + '.input2')
            cmds.connectAttr(driven_switch + '.output', sdk_attr_name)
            if mirror:
                sdkNode_output_mirror = cmds.listConnections(sdk_attr_name_mirror, plugs=True, s=True, d=False)[0]
                cmds.disconnectAttr(sdkNode_output_mirror, sdk_attr_name_mirror)
                cmds.connectAttr(sdkNode_output_mirror, driven_switch_mirror + '.input2')
                cmds.connectAttr(driven_switch_mirror + '.output', sdk_attr_name_mirror)

        cmds.currentTime(1)
        keyframes = cmds.keyframe(driven, query=True)

        if keyframes:
            cmds.cutKey(driven, clear=True)
            print("已删除物体 '{}' 的所有关键帧。".format(driven))
        else:
            print("物体 '{}' 没有找到任何关键帧。".format(driven))

        parent_grp = cmds.listRelatives(driven, parent=True)[0]
        for attr in ['tx', 'ty', 'tz', 'rx', 'ry', 'rz']:
            cmds.setAttr(driven + '.' + attr, 0)
            cmds.setAttr(parent_grp + '.' + attr, 0)

        for attr in ['sx', 'sy', 'sz']:
            cmds.setAttr(driven + '.' + attr, 1)


def run_anim_to_sdk():
    """选中被驱动控制器（可多选），把已 k 好的动画转为 SDK 驱动。
    运行前确保第一帧时属性值为 0，被驱动属性只能是位移/旋转/缩放。"""
    drivens = cmds.ls(selection=True)
    if not drivens:
        cmds.warning("请先选择被驱动的控制器。")
        return

    result = prompt_fields(
        "动画转 SDK",
        [("驱动属性 (如 ctrl_m_head_001.rotateX)", ""),
         ("被驱动属性 (空格分隔)", "tx ty tz rx ry rz sx sy sz"),
         ("镜像 (1/0)", "1")])
    if result is None:
        return

    driver_attr = result[0].strip()
    if not driver_attr:
        cmds.warning("请填写驱动属性。")
        return
    driven_attrs = result[1].split()
    mirror = bool(int(result[2]) if result[2].strip() in ('0', '1') else 1)

    anim_to_SDK(drivens, driven_attrs, driver_attr, mirror)
def valMatch(ctrlAttr, jntAttr, matchAttrVal, addVal=0.01):
    """通过移动 IK 控制器，使关节某一轴的旋转数值稳定到目标值（通常用于匹配脚步）。

    :param ctrlAttr: (str) 控制器控制关节的属性。
    :param jntAttr: (str) 关节的属性。
    :param matchAttrVal: (float) 需要稳定的关节属性目标数值。
    :param addVal: (float) 步长，默认 0.01。
    """
    ctrlAttr_val = cmds.getAttr(ctrlAttr)
    jntAttr_val = cmds.getAttr(jntAttr)

    for i in range(100):
        if jntAttr_val < matchAttrVal:
            cmds.setAttr(ctrlAttr, ctrlAttr_val - addVal)
            jntAttr_val_a = cmds.getAttr(jntAttr)
            if jntAttr_val_a < jntAttr_val:
                cmds.setAttr(ctrlAttr, ctrlAttr_val + 2 * addVal)

        if jntAttr_val > matchAttrVal:
            cmds.setAttr(ctrlAttr, ctrlAttr_val - addVal)
            jntAttr_val_a = cmds.getAttr(jntAttr)
            if jntAttr_val_a > jntAttr_val:
                cmds.setAttr(ctrlAttr, ctrlAttr_val + 2 * addVal)

        jntAttr_val = cmds.getAttr(jntAttr)


def run_match_attr_value():
    """通过迭代移动 IK 控制器，使关节某轴数值稳定到目标值。"""
    result = prompt_fields(
        "匹配属性值",
        [("控制器属性 (如 ctrl_l_foot_001.tx)", ""),
         ("关节属性 (如 jnt_l_foot_001.rotateX)", ""),
         ("目标数值", "0")])
    if result is None:
        return

    ctrl_attr = result[0].strip()
    jnt_attr = result[1].strip()
    match_val = float(result[2]) if result[2].strip() else 0.0

    if not ctrl_attr or not jnt_attr:
        cmds.warning("请填写控制器属性和关节属性。")
        return

    valMatch(ctrl_attr, jnt_attr, match_val)
connection_data = {}


def record_connections(*args):
    global connection_data
    selected_objects = cmds.ls(selection=True)

    if not selected_objects:
        cmds.warning("请先选择至少一个物体。")
        return

    channel_box = mel.eval('$tmp = $gChannelBoxName')
    selected_attrs = cmds.channelBox(channel_box, query=True, selectedMainAttributes=True)

    connection_data = {}

    for obj in selected_objects:
        if not selected_attrs:
            cmds.warning("请在 Channel Box 中选择一些属性。")
            return

        for attr in selected_attrs:
            full_attr = obj + "." + attr
            connections = cmds.listConnections(full_attr, plugs=True)
            if connections:
                connection_data[full_attr] = connections

    cmds.confirmDialog(title='成功', message='已记录连接。')


def toggle_connections(*args):
    if not connection_data:
        cmds.warning("请先记录连接。")
        return

    for full_attr, connections in connection_data.items():
        if cmds.listConnections(full_attr):
            for connection in connections:
                cmds.disconnectAttr(full_attr, connection)
        else:
            if connections:
                for connection in connections:
                    cmds.connectAttr(full_attr, connection)
    cmds.confirmDialog(title='成功', message='已切换连接。')


def show_attr_connect_window():
    if cmds.window("myWindow", exists=True):
        cmds.deleteUI("myWindow")

    window = cmds.window("myWindow", title="属性连接管理", widthHeight=(300, 150))
    cmds.columnLayout(adjustableColumn=True)

    cmds.button(label='记录连接', command=record_connections)
    cmds.button(label='断开/重新连接', command=toggle_connections)

    cmds.showWindow(window)


def run_attr_connect_toggle():
    """在 Channel Box 选中属性后点「记录连接」保存连接，再用「断开/重新连接」切换。"""
    show_attr_connect_window()
def add_visibility_attribute(*args):
    selected_objects = cmds.ls(selection=True)

    if not selected_objects:
        cmds.warning("请先选择至少一个控制器。")
        return

    attr_name = cmds.textField('attrNameField', query=True, text=True)

    if not attr_name:
        cmds.warning("请输入属性名称。")
        return

    master_controller = selected_objects[-1]

    if not cmds.attributeQuery(attr_name, node=master_controller, exists=True):
        cmds.addAttr(master_controller, longName=attr_name, attributeType='bool', defaultValue=False)
        cmds.setAttr('{}.{}'.format(master_controller, attr_name), keyable=True)

    for controller in selected_objects[:-1]:
        parent_level = cmds.listRelatives(controller, parent=True, fullPath=True)
        if parent_level:
            grandparent = cmds.listRelatives(parent_level[0], parent=True, fullPath=True)
            if grandparent:
                cmds.connectAttr('{}.{}'.format(master_controller, attr_name), '{}.visibility'.format(grandparent[0]))

    cmds.confirmDialog(title='成功', message='已为主控制器添加属性并连接其他控制器可见性。')


def show_ctrl_visibility_window():
    if cmds.window("myWindow", exists=True):
        cmds.deleteUI("myWindow")

    window = cmds.window("myWindow", title="控制器显示/隐藏", widthHeight=(300, 150))
    cmds.columnLayout(adjustableColumn=True)

    cmds.text(label='输入属性名称：')
    cmds.textField('attrNameField')

    cmds.button(label='添加控制器可见性属性', command=add_visibility_attribute)

    cmds.showWindow(window)


def run_ctrl_visibility_attr():
    """先选要隐藏的控制器，再加选主控制器，给主控制器添加属性并连接到各控制器的第二父级组可见性。"""
    show_ctrl_visibility_window()
def run_break_inverse_scale():
    """断开选中关节的 inverseScale 连接。"""
    jnts = cmds.ls(selection=True)
    if not jnts:
        cmds.warning("请先选择关节。")
        return

    for jnt in jnts:
        inverse_scale_connections = cmds.listConnections(jnt + ".inverseScale", s=True, d=False)
        if inverse_scale_connections:
            for conn in inverse_scale_connections:
                cmds.disconnectAttr(conn + ".scale", jnt + ".inverseScale")
def rename_objects(prefix, suffix, head, start_number=1):
    selection = cmds.ls(selection=True)
    if not selection:
        cmds.warning("请先选择要重命名的物体。")
        return

    for obj in selection:
        rename_recursively(obj, prefix, suffix, head, start_number)


def rename_recursively(obj, prefix, suffix, head, number):
    new_name = "{}_{}_{}_{:03d}".format(prefix, suffix, head, number)

    children = cmds.listRelatives(obj, children=True) or []

    cmds.rename(obj, new_name)

    for index, child in enumerate(children, start=1):
        rename_recursively(child, prefix, suffix, head, number + index)


def on_rename_button_clicked(*args):
    prefix = cmds.textField('prefixField', query=True, text=True)
    suffix = cmds.textField('suffixField', query=True, text=True)
    head = cmds.textField('headField', query=True, text=True)

    rename_objects(prefix, suffix, head)


def show_rename_window():
    if cmds.window("renameWindow", exists=True):
        cmds.deleteUI("renameWindow")

    cmds.window("renameWindow", title="重命名工具", widthHeight=(300, 150))

    cmds.columnLayout(adjustableColumn=True)

    cmds.textField('prefixField', placeholderText='请输入 mesh')
    cmds.textField('suffixField', placeholderText='请输入 l')
    cmds.textField('headField', placeholderText='请输入 head')

    cmds.button(label="重命名", command=on_rename_button_clicked)

    cmds.setParent('..')
    cmds.showWindow("renameWindow")


def run_rename_objects():
    """选中物体，输入前缀/方向/名称，按 prefix_side_name_编号 递归重命名。"""
    show_rename_window()
def run_move_pivot_to_origin():
    """把选中模型的枢轴设置到世界原点。"""
    selection = cmds.ls(selection=True)

    if not selection:
        cmds.warning("请先选择至少一个模型。")
        return

    for obj in selection:
        if cmds.objectType(obj) == "transform":
            cmds.xform(obj, piv=(0, 0, 0), ws=True)

            cmds.xform(obj, worldSpace=True, translation=cmds.xform(obj, query=True, translation=True, ws=True))


# ============================ 工具注册表 ============================
# (分类, 按钮名, 悬浮提示, 调用函数)
TOOLS = [
    ("控制器", "标准控制器层级", '选中控制器曲线，生成 zero/driven/connect/offset 层级，并复制子控制器、创建 output 组连接子控制器属性。', run_ctrl_standard_hierarchy),
    ("控制器", "层级并自动父子", '选中多个控制器（命名含编号），生成标准层级并按编号自动父子（编号大的 zero 组挂到上一层控制器下）。', run_ctrl_hierarchy_auto_parent),
    ("控制器", "控制器绑定骨骼链", '先选骨骼链最上层关节，再加选一个控制器曲线，为每根骨骼复制控制器、建层级并用控制器约束骨骼。', run_bind_ctrls_to_joint_chain),
    ("控制器", "模型归入控制组", '先选模型（可多选），再加选关节，把模型归入该关节对应的控制组，没有则自动创建。', run_parent_mesh_to_ctrl_grp),
    ("控制器", "曲线生成控制骨骼", '选中 5 个 CV 点的曲线，在两端与中心生成三个主控制关节（朝向需手动调整）。', run_ctrl_joints_from_curves),
    ("控制器", "Picker固定到控制器", '先选 picker 曲线，再加选控制器，把 picker 形状固定到控制器上（跟随控制器但不变形）。', run_picker_to_ctrl),
    ("控制器", "空间切换", '选中控制器，添加 space 空间切换属性（选项如 chest/root/world/shoulder/hip）。', run_ctrl_space_switch),
    ("模型/蒙皮", "模型匹配关节名", '先选骨骼（格式 description + L/R），再加选模型，新建命名组并把模型约束到该关节（父子+缩放约束）。', run_mesh_match_joint_name),
    ("模型/蒙皮", "约束组转蒙皮", '选中 grp 组（grp_l_description_001），按组名推断关节并对组下模型做蒙皮，把约束驱动改成蒙皮驱动。', run_constraint_grp_to_skincluster),
    ("模型/蒙皮", "传递蒙皮权重", '先选有权重的模型，再加选目标模型，把源蒙皮权重拷贝到目标模型。', run_transfer_skin_weights),
    ("曲线/骨骼", "关节钉在曲线CV", '选中一条曲线（可选加选已有骨骼），把骨骼钉在曲线 CV 点上；只选曲线则按 CV 生成新骨骼。', run_attach_joints_on_cv),
    ("曲线/骨骼", "关节附着+aim曲线", '选关节（可多选）+ drive 曲线 + aim 曲线 + 上方向参考物体，用 pointOnCurveInfo 附着并 aim 约束。', run_attach_joints_aim_curve),
    ("曲线/骨骼", "曲线生成骨骼(切线)", '先选驱动曲线，再加选上方向曲线，生成沿曲线切线朝向的骨骼链（可设数量）。', run_joints_on_curve_tangent),
    ("曲线/骨骼", "管状绑定", '先选驱动曲线，再加选上方向曲线（可再加选 3 个控制骨骼），生成附着骨骼并可选镜像。', run_tube_rig),
    ("曲线/骨骼", "胡须/触须绑定", '选主控制器 + drive 曲线 + aim 曲线 + up 曲线（最后三个为曲线），用 aim 曲线驱动并生成控制曲线。', run_tentacle_beard_rig),
    ("腿部绑定", "人腿 IK/FK", '选中人腿最上层关节，自动生成 IK/FK 控制器、极向量及 FKIK 切换系统。', run_human_leg_rig),
    ("腿部绑定", "马腿 springIK+SC", '选中马腿最上层关节（至少 4 个关节），自动生成 springIK + SC IK 与 FK 及 FKIK 切换系统。', run_horse_leg_rig),
    ("触须特效", "涟漪缩放", '给触须驱动控制器添加涟漪缩放属性（依赖 MASH 插件，关节命名 jnt_m_tentacle_???）。', run_tentacle_ripple),
    ("触须特效", "卷曲", '给触须控制器添加卷曲属性（依赖 MASH 插件，控制器命名 ctrl_m_tentacle_???）。', run_tentacle_roll),
    ("动画/属性", "运动轨迹locator", '选中物体，在每一帧生成一个 locator（粉色）并归入 Locator_Group 组。', run_anim_path_locators),
    ("动画/属性", "动画转SDK", '选中被驱动控制器，把已 k 好的动画转为 SDK 驱动；运行前第一帧属性值需为 0。', run_anim_to_sdk),
    ("动画/属性", "匹配属性值", '输入控制器属性与关节属性及目标值，迭代移动 IK 控制器使关节某轴数值稳定到目标。', run_match_attr_value),
    ("动画/属性", "属性连接开关", '在 Channel Box 选中属性后点「记录连接」，再用「断开/重新连接」切换连接状态。', run_attr_connect_toggle),
    ("动画/属性", "控制器显隐属性", '先选要隐藏的控制器，再加选主控制器，输入属性名并连接到各控制器第二父级组的可见性。', run_ctrl_visibility_attr),
    ("动画/属性", "断开inverseScale", '断开选中关节的 inverseScale 连接。', run_break_inverse_scale),
    ("动画/属性", "批量重命名", '选中物体，输入前缀/方向/名称，按 prefix_side_name_编号 递归重命名。', run_rename_objects),
    ("动画/属性", "轴心归零", '把选中模型的枢轴设置到世界原点。', run_move_pivot_to_origin),
]


def _run_tool(func):
    try:
        func()
    except Exception as exc:
        cmds.warning("运行工具失败：{}".format(exc))
        traceback.print_exc()


def _get_maya_main_window():
    return wrapInstance(int(OpenMayaUI.MQtUtil.mainWindow()), QtWidgets.QWidget)


class ToolboxWidget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(ToolboxWidget, self).__init__(parent)
        self.setObjectName("dabaieToolbox")
        self.setWindowTitle("大白鹅 Maya 工具箱")

        outer = QtWidgets.QVBoxLayout(self)
        outer.setContentsMargins(8, 8, 8, 8)

        scroll = QtWidgets.QScrollArea()
        scroll.setWidgetResizable(True)
        scroll.setFrameShape(QtWidgets.QFrame.NoFrame)
        outer.addWidget(scroll)

        container = QtWidgets.QWidget()
        scroll.setWidget(container)
        main_layout = QtWidgets.QVBoxLayout(container)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(8)

        current_category = None
        group_box = None
        grid = None
        count = 0

        for category, label, tooltip, func in TOOLS:
            if category != current_category:
                current_category = category
                count = 0
                group_box = QtWidgets.QGroupBox(category)
                main_layout.addWidget(group_box)
                grid = QtWidgets.QGridLayout(group_box)
                grid.setSpacing(4)

            btn = QtWidgets.QPushButton(label)
            btn.setToolTip(tooltip)
            btn.setMinimumHeight(26)
            btn.clicked.connect(lambda checked=False, f=func: _run_tool(f))
            grid.addWidget(btn, count // 3, count % 3)
            count += 1

        main_layout.addStretch(1)


if MayaQWidgetDockableMixin is not None:
    _DockBases = (MayaQWidgetDockableMixin, QtWidgets.QWidget)
else:
    _DockBases = (QtWidgets.QWidget,)


class ToolboxDock(*_DockBases):
    def __init__(self, parent=None):
        super(ToolboxDock, self).__init__(parent=parent)
        self.setObjectName("dabaieToolboxDock")
        layout = QtWidgets.QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.addWidget(ToolboxWidget(self))


_win = None


def show():
    """弹出（或重新聚焦）工具箱窗口。"""
    global _win
    if _win is None:
        _win = ToolboxDock(parent=_get_maya_main_window())
    _win.show()
    _win.raise_()
    return _win


def add_shelf_button():
    """在当前 shelf 添加「大白鹅工具箱」按钮。"""
    top_shelf = mel.eval('$tmp = $gShelfTopLevel')
    current = cmds.tabLayout(top_shelf, query=True, selectTab=True)
    cmds.shelfButton(
        parent=current,
        image='commandButton.png',
        label='工具箱',
        annotation='大白鹅 Maya 工具箱',
        command='import dabaieToolbox_single; dabaieToolbox_single.show()',
        sourceType='python',
    )


def install():
    """安装入口：添加 shelf 按钮（不自动弹出窗口）。"""
    add_shelf_button()
    print("大白鹅 Maya 工具箱已安装，点击 shelf「工具箱」按钮打开。")


show()
