Files
blender-portable-repo/scripts/addons/Animtoolbox/Rigger_Toolbox.py
T
2026-03-17 14:30:01 -06:00

549 lines
26 KiB
Python

# ***** BEGIN GPL LICENSE BLOCK *****
#
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ***** END GPL LICENCE BLOCK *****
import bpy
from mathutils import Matrix, Vector
from math import radians
import numpy
def draw_wgt(boneLength, bone):
suffix = bone.id_data.name + '_' + bone.name
if 'WGTB_object' + suffix in bpy.data.objects:
obj = bpy.data.objects['WGTB_object'] + suffix
if 'WGTB_shape' + suffix in obj.data.name:
return obj
mesh = bpy.data.meshes.new('WGTB_shape_' + suffix)
obj = bpy.data.objects.new('WGTB_object_' + suffix, mesh)
#coordinates of the sphere widget shape
sphere = {"edges": [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10], [10, 11], [11, 12], [12, 13], [13, 14], [14, 15], [15, 16], [16, 17], [17, 18], [18, 19], [19, 20], [20, 21], [21, 22], [22, 23], [0, 23], [24, 25], [25, 26], [26, 27], [27, 28], [28, 29], [29, 30], [30, 31], [31, 32], [32, 33], [33, 34], [34, 35], [35, 36], [36, 37], [37, 38], [38, 39], [39, 40], [40, 41], [41, 42], [42, 43], [43, 44], [44, 45], [45, 46], [46, 47], [24, 47], [48, 49], [49, 50], [50, 51], [51, 52], [52, 53], [53, 54], [54, 55], [55, 56], [56, 57], [57, 58], [58, 59], [59, 60], [60, 61], [61, 62], [62, 63], [63, 64], [64, 65], [65, 66], [66, 67], [67, 68], [68, 69], [69, 70], [70, 71], [48, 71]],
"vertices": [[0.0, 0.10000002384185791, 0.0], [-0.025881901383399963, 0.09659260511398315, 0.0], [-0.050000011920928955, 0.08660250902175903, 0.0], [-0.07071065902709961, 0.07071065902709961, 0.0], [-0.08660256862640381, 0.04999998211860657, 0.0], [-0.09659260511398315, 0.025881901383399963, 0.0], [-0.10000002384185791, 7.549793679118011e-09, 0.0], [-0.09659260511398315, -0.02588188648223877, 0.0], [-0.08660256862640381, -0.04999998211860657, 0.0], [-0.07071071863174438, -0.07071065902709961, 0.0], [-0.050000011920928955, -0.08660250902175903, 0.0], [-0.02588193118572235, -0.09659260511398315, 0.0], [-3.894143674187944e-08, -0.10000002384185791, 0.0], [0.025881856679916382, -0.09659260511398315, 0.0], [0.04999995231628418, -0.08660256862640381, 0.0], [0.07071065902709961, -0.07071071863174438, 0.0], [0.08660250902175903, -0.05000004172325134, 0.0], [0.09659254550933838, -0.025881946086883545, 0.0], [0.10000002384185791, -4.649123752642481e-08, 0.0], [0.09659260511398315, 0.025881856679916382, 0.0], [0.08660256862640381, 0.04999995231628418, 0.0], [0.07071071863174438, 0.07071065902709961, 0.0], [0.05000007152557373, 0.08660250902175903, 0.0], [0.025881975889205933, 0.09659254550933838, 0.0], [0.0, 7.450580596923828e-09, 0.10000002384185791], [-0.025881901383399963, 7.450580596923828e-09, 0.09659260511398315], [-0.050000011920928955, 7.450580596923828e-09, 0.08660250902175903], [-0.07071065902709961, 7.450580596923828e-09, 0.07071065902709961], [-0.08660256862640381, 3.725290298461914e-09, 0.04999998211860657], [-0.09659260511398315, 1.862645149230957e-09, 0.025881901383399963], [-0.10000002384185791, 8.881784197001252e-16, 7.549793679118011e-09], [-0.09659260511398315, -1.862645149230957e-09, -0.02588188648223877], [-0.08660256862640381, -3.725290298461914e-09, -0.04999998211860657], [-0.07071071863174438, -7.450580596923828e-09, -0.07071065902709961], [-0.050000011920928955, -7.450580596923828e-09, -0.08660250902175903], [-0.02588193118572235, -7.450580596923828e-09, -0.09659260511398315], [-3.894143674187944e-08, -7.450580596923828e-09, -0.10000002384185791], [0.025881856679916382, -7.450580596923828e-09, -0.09659260511398315], [0.04999995231628418, -7.450580596923828e-09, -0.08660256862640381], [0.07071065902709961, -7.450580596923828e-09, -0.07071071863174438], [0.08660250902175903, -3.725290298461914e-09, -0.05000004172325134], [0.09659254550933838, -1.862645149230957e-09, -0.025881946086883545], [0.10000002384185791, -3.552713678800501e-15, -4.649123752642481e-08], [0.09659260511398315, 1.862645149230957e-09, 0.025881856679916382], [0.08660256862640381, 3.725290298461914e-09, 0.04999995231628418], [0.07071071863174438, 7.450580596923828e-09, 0.07071065902709961], [0.05000007152557373, 7.450580596923828e-09, 0.08660250902175903], [0.025881975889205933, 7.450580596923828e-09, 0.09659254550933838], [-7.450580596923828e-09, 4.440892098500626e-16, 0.10000002384185791], [-9.313225746154785e-09, -0.025881901383399963, 0.09659260511398315], [-1.1175870895385742e-08, -0.050000011920928955, 0.08660250902175903], [-1.4901161193847656e-08, -0.07071065902709961, 0.07071065902709961], [-7.450580596923828e-09, -0.08660256862640381, 0.04999998211860657], [-7.450580596923828e-09, -0.09659260511398315, 0.025881901383399963], [-7.450580596923828e-09, -0.10000002384185791, 7.549793679118011e-09], [-7.450580596923828e-09, -0.09659260511398315, -0.02588188648223877], [0.0, -0.08660256862640381, -0.04999998211860657], [0.0, -0.07071071863174438, -0.07071065902709961], [3.725290298461914e-09, -0.050000011920928955, -0.08660250902175903], [5.587935447692871e-09, -0.02588193118572235, -0.09659260511398315], [7.450577044210149e-09, -3.894143674187944e-08, -0.10000002384185791], [9.313225746154785e-09, 0.025881856679916382, -0.09659260511398315], [1.1175870895385742e-08, 0.04999995231628418, -0.08660256862640381], [1.4901161193847656e-08, 0.07071065902709961, -0.07071071863174438], [7.450580596923828e-09, 0.08660250902175903, -0.05000004172325134], [7.450580596923828e-09, 0.09659254550933838, -0.025881946086883545], [7.450580596923828e-09, 0.10000002384185791, -4.649123752642481e-08], [7.450580596923828e-09, 0.09659260511398315, 0.025881856679916382], [0.0, 0.08660256862640381, 0.04999995231628418], [0.0, 0.07071071863174438, 0.07071065902709961], [-3.725290298461914e-09, 0.05000007152557373, 0.08660250902175903], [-5.587935447692871e-09, 0.025881975889205933, 0.09659254550933838]], "faces": []}
mesh.from_pydata(numpy.array(sphere['vertices'])*[boneLength, boneLength, boneLength] , sphere['edges'], sphere['faces'])
return obj
def add_driver(obj, posebone, control, target, path, multiply = ''):
if isinstance(target, tuple):
attr = posebone.driver_add(target[0], target[1])
else:
attr = posebone.driver_add(target)
var = attr.driver.variables.new()
var.targets[0].id = obj
var.targets[0].data_path = 'pose.bones["' + control +'"].'+ path
attr.driver.expression = var.name + multiply
def dup_values(source, target):
if hasattr(source, 'parent'):
target.parent = source.parent
for prop in dir(source):
if not hasattr(target, prop):
continue
value = getattr(source, prop)
if type(value) not in {int, float, bool, str, Vector, Matrix, bpy.types.Object}:
continue
if '__' in prop[:2] and '__' in prop[-2:]:
continue
if target.is_property_readonly(prop):
continue
setattr(target, prop, value)
return target
def dup_constraints(source, target):
if not source.constraints.items():
return
for source_con in source.constraints:
target_con = target.constraints.new(source_con.type)
dup_values(source_con, target_con)
def add_vis_bone_con(obj, bone_vis_name, bone_wgt_name):
bone_vis = obj.pose.bones[bone_vis_name]
con = bone_vis.constraints.new('STRETCH_TO')
con.target = obj
con.subtarget = bone_wgt_name
return bone_vis
class target:
def __init__(self, bone):
self.name = bone.name
self.point = tuple(bone.tail)
self.ctrl = 'TRGT_' + bone.name
if bone.parent:
self.parent = bone.parent.name
#print('assign parent to target ', self.name, self.ctrl, self.parent)
def __lt__(self, other):
return self.point < other.point
def __hash__(self):
return hash(self.point)
def __eq__(self, other):
#if not isinstance(other, type(self)):
# return NotImplemented
return self.point == other.point
class parent:
def __init__(self, bone):
self.name = bone.name
self.point = tuple(bone.head)
self.ctrl = 'CTRL_' + bone.name
if bone.parent:
self.parent = bone.parent.name
def __lt__(self, other):
return self.point < other.point
def __hash__(self):
return hash(self.point)
def __eq__(self, other):
#if not isinstance(other, type(self)):
# return NotImplemented
return self.point == other.point
class constraint_dup:
def __init__(self, bone, con):
self.name = con.name
self.target = con.target
self.subtarget = con.subtarget
self.bone = bone.name
def __hash__(self):
return hash(self.bone)
def __eq__(self, other):
return self.bone == other.bone
def bone_orientation(source, target, value):
source.align_orientation(target)
x, y, z = source.matrix.to_3x3().col
R = (Matrix.Translation(source.head) @ Matrix.Rotation(radians(value), 4, x) @ Matrix.Translation(-source.head))
source.transform(R, roll = False)
source.align_roll(target.vector)
def find_ctrl(bone, controls):
i = list(controls).index(bone)
bone.ctrl = list(controls)[i].ctrl
return bone.ctrl
def add_controlers(self, obj, parents, targets):
#controls = set(parents).union(targets)
controls = set(parents + targets)
#create hierarchy
for bone in controls:
editbone = obj.data.edit_bones[bone.name]
if editbone.parent is None:
continue
parentnames = [bone.name for bone in parents]
#if a target and its parent are part of the hierarchy then linked to its own bone parent
if bone in targets and bone not in parents and editbone.parent.name in parentnames:
parentbone = parent(editbone)
else:
parentbone = parent(editbone.parent)
if parentbone in controls and parentbone != bone:
bone.parent = find_ctrl(parentbone, controls)
else:
bone.parent = editbone.parent.name
edit_bones = obj.data.edit_bones
for bone in controls:
editbone = edit_bones[bone.name]
ctrl = obj.data.edit_bones.new(bone.ctrl)
ctrl.head = bone.point
ctrl.tail = bone.point
ctrl.tail[2] = bone.point[2] + (editbone.length / 3)
ctrl.bbone_x = editbone.bbone_x
ctrl.bbone_z = editbone.bbone_z
ctrl.use_deform = False
if self.bone_align:
angle = 90 if self.align_90 else 0
bone_orientation(ctrl, editbone, angle)
if angle == 90:
ctrl.align_roll(editbone.vector)
else:
ctrl.roll = editbone.roll
#apply hierarchy
for bone in parents:
editbone = edit_bones[bone.name]
ctrl_name = find_ctrl(bone, controls)
ctrl = edit_bones[ctrl_name]
editbone.parent = ctrl
for bone in controls:
ctrl = edit_bones[bone.ctrl]
if hasattr(bone, 'parent'):
ctrl.parent = edit_bones[bone.parent]
return controls
def pose_bbone_setup(bone, posebone, bbone_group = None):
#add the custom shape to the widget bones
custom_shape = draw_wgt(bone['length'], posebone)
posebone.custom_shape = custom_shape
posebone.use_custom_shape_bone_size = False
if bbone_group:
posebone.bone_group = bbone_group
posebone.rotation_mode = 'XZY'
posebone.lock_rotation[0] = True
posebone.lock_rotation[2] = True
#####MAIN####
class BboneWidgets(bpy.types.Operator):
"""Add Bbone widget controls to the selected bones"""
bl_idname = "armature.add_bbone_widgets"
bl_label = "Add_Bbone_widgets"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return bpy.context.object.type == 'ARMATURE'
def execute(self, context):
obj = context.object
obj.data.display_type = 'BBONE'
bones = []
parentlayers = [False if i != 24 else True for i in range(32)]
wgtlayers = [True if i == 0 else False for i in range(32)]
bpy.ops.object.mode_set(mode = 'EDIT')
obj.data.use_mirror_x = False
for bone in obj.data.edit_bones:
if not bone.select:
continue
if bone.bbone_segments == 1:
bone.bbone_segments = 10
bone.bbone_handle_type_start = 'TANGENT'
bone.bbone_handle_type_end = 'TANGENT'
bone_name = bone.name
#add parent bone to the Bbone widgets
parent = obj.data.edit_bones.new('WGTB_parent_'+ bone_name)
parent_name = parent.name
dup_values(bone, parent)
parent.name = parent_name
parent.select = False
parent.select_head = False
parent.select_tail = False
#change layer of the parent bone
if bpy.app.version < (4, 0, 0):
parent.layers = parentlayers
#add bbone widget bones
head_widget = obj.data.edit_bones.new('Bhead_'+ bone.name)
head_widget.parent = parent
#head_widget.head = bone.head
head_widget.head = bone.head + (bone.tail - bone.head) * 0.25
#head_widget.tail = (bone.tail - bone.head)/10
head_widget.length = bone.length * 0.1
head_widget.bbone_x = bone.bbone_x
head_widget.bbone_z = bone.bbone_z
head_widget.align_orientation(bone)
head_widget.inherit_scale = 'NONE'
head_name = head_widget.name
tail_widget = obj.data.edit_bones.new('Btail_'+ bone.name)
tail_widget.parent = parent
#tail_widget.head = bone.tail
tail_widget.head = bone.head + (bone.tail - bone.head) * 0.75
#tail_widget.tail = bone.tail - (bone.tail - bone.head)/10
tail_widget.length = bone.length * 0.1
tail_widget.bbone_x = bone.bbone_x
tail_widget.bbone_z = bone.bbone_z
tail_widget.align_orientation(bone)
tail_widget.inherit_scale = 'NONE'
tail_name = tail_widget.name
#add vis bones
head_vis = obj.data.edit_bones.new('Bhead_vis_'+ bone.name)
head_vis.parent = parent
head_vis.head = bone.head
head_vis.tail = head_widget.head
head_vis.bbone_x = bone.bbone_x*0.1
head_vis.bbone_z = bone.bbone_z*0.1
#head_vis_name = head_vis.name
head_vis.hide_select = True
head_vis.use_deform = False
tail_vis = obj.data.edit_bones.new('Btail_vis_'+ bone.name)
tail_vis.parent = parent
tail_vis.head = bone.tail
tail_vis.tail = tail_widget.head
tail_vis.bbone_x = bone.bbone_x*0.1
tail_vis.bbone_z = bone.bbone_z*0.1
#tail_vis_name = tail_vis.name
tail_vis.hide_select = True
tail_vis.use_deform = False
if bpy.app.version < (4, 0, 0):
tail_widget.layers = wgtlayers
head_widget.layers = wgtlayers
head_vis.layers = wgtlayers
tail_vis.layers = wgtlayers
bones.append({'name': bone_name, 'parent': parent_name, 'head': head_name, 'tail': tail_name, 'head_vis': head_vis.name, 'tail_vis': tail_vis.name, 'length': bone.length})
#####POSE MODE#######
bpy.ops.object.mode_set(mode = 'POSE')
if bpy.app.version < (4, 0, 0):
bone_groups = obj.pose.bone_groups
if 'BBone Widgets' not in bone_groups:
bbone_group = bone_groups.new(name = 'BBone Widgets')
bbone_group.color_set = 'THEME09'
else:
bbone_group = bone_groups['BBone Widgets']
else:
bbone_group = None
for bone in bones:
posebone = obj.pose.bones[bone['name']]
# Prepare parent bone in pose mode
poseparent = obj.pose.bones[bone['parent']]
#disable use deform
obj.data.bones[bone['parent']].use_deform = False
obj.data.bones[bone['head']].use_deform = False
obj.data.bones[bone['tail']].use_deform = False
pose_bbone_setup(bone, obj.pose.bones[bone['head']], bbone_group)
pose_bbone_setup(bone, obj.pose.bones[bone['tail']], bbone_group)
dup_constraints(posebone, poseparent)
#add all the drivers
add_driver(obj, posebone, bone['head'], 'bbone_curveinx', 'location.x')
add_driver(obj, posebone, bone['head'], 'bbone_curveinz', 'location.z')
add_driver(obj, posebone, bone['head'], 'bbone_easein', 'location.y', '*5/'+ str(bone['length']))
add_driver(obj, posebone, bone['head'], 'bbone_rollin', 'rotation_euler.y')
add_driver(obj, posebone, bone['head'], ('bbone_scalein', 0), 'scale.x')
add_driver(obj, posebone, bone['head'], ('bbone_scalein', 1), 'scale.y')
add_driver(obj, posebone, bone['head'], ('bbone_scalein', 2), 'scale.z')
add_driver(obj, posebone, bone['tail'], 'bbone_curveoutx', 'location.x')
add_driver(obj, posebone, bone['tail'], 'bbone_curveoutz', 'location.z')
add_driver(obj, posebone, bone['tail'], 'bbone_easeout', 'location.y', '*-5/'+ str(bone['length']))
add_driver(obj, posebone, bone['tail'], 'bbone_rollout', 'rotation_euler.y')
add_driver(obj, posebone, bone['tail'], ('bbone_scaleout', 0), 'scale.x')
add_driver(obj, posebone, bone['tail'], ('bbone_scaleout', 1), 'scale.y')
add_driver(obj, posebone, bone['tail'], ('bbone_scaleout', 2), 'scale.z')
#add constraints to visual bones
head_vis = add_vis_bone_con(obj, bone['head_vis'], bone['head'])
tail_vis = add_vis_bone_con(obj, bone['tail_vis'], bone['tail'])
if bpy.app.version < (4, 0, 0):
head_vis.bone_group = bbone_group
tail_vis.bone_group = bbone_group
return {"FINISHED"}
class ChainControls(bpy.types.Operator):
"""Add parent and target controls to the selected bones to create a chain control"""
bl_idname = "armature.add_chain_ctrls"
bl_label = "Add_Chain_Controls"
bl_options = {'REGISTER', 'UNDO'}
parents: bpy.props.BoolProperty(name = 'Add Parents', description = "Align the controls 90 degrees to the original bones", default = True)
targets: bpy.props.BoolProperty(name = 'Add Targets', description = "Align the controls 90 degrees to the original bones", default = True)
keep_hierarchy: bpy.props.BoolProperty(name = 'Keep Hierarchy', description = "Keep the controls in the hierarchy of the original bones", default = True)
bone_align: bpy.props.BoolProperty(name = 'Align to Bones', description = "Align the controls to the original bones", default = True)
align_90: bpy.props.BoolProperty(name = '+90°', description = "Align the controls 90 degrees to the original bones", default = True)
@classmethod
def poll(cls, context):
return bpy.context.object.type == 'ARMATURE'
def invoke(self, context, event):
#obj = context.object
wm = context.window_manager
return wm.invoke_props_dialog(self, width = 200)
def draw(self, context):
layout = self.layout
row = layout.row()
row.label(text = 'Add Control Bones')
row = layout.row()
row.prop(self, 'parents') #text = 'Size'
row.prop(self, 'targets')
layout.separator()
col = layout.column()
col.prop(self, 'keep_hierarchy')
row = layout.row()
row.prop(self, 'bone_align')
if self.bone_align:
row.prop(self, 'align_90', toggle=True)
def execute(self, context):
obj = context.object
targets = []
parents = []
bpy.ops.object.mode_set(mode = 'EDIT')
edit_bones = bpy.context.selected_editable_bones
#create list of parent and target objects
for bone in edit_bones:
bone.use_connect = False
if self.targets:
targets.append(target(bone))
if self.parents:
parents.append(parent(bone))
controls = add_controlers(self, obj, parents, targets)
bpy.ops.object.mode_set(mode = 'POSE')
#Add the bone group for the ctrls if doesn't exist
if bpy.app.version < (4, 0, 0):
bone_groups = obj.pose.bone_groups
if 'Ctrl Bones' not in bone_groups:
ctrl_group = bone_groups.new(name = 'Ctrl Bones')
ctrl_group.color_set = 'THEME01'
else:
ctrl_group = bone_groups['Ctrl Bones']
for bone in controls:
posebone = obj.pose.bones[bone.ctrl]
if bpy.app.version < (4, 0, 0):
posebone.bone_group = ctrl_group
else:
posebone.color.palette = 'THEME01'
if self.targets:
for bone in targets:
#update from the controls set
ctrl = find_ctrl(bone, controls)
posebone = obj.pose.bones[bone.name]
con = posebone.constraints.new('STRETCH_TO')
con.target = obj
con.subtarget = ctrl
return {"FINISHED"}
class MergeRigs(bpy.types.Operator):
"""Merge selected rigs to active and keep hierarchy and constraints for shared bones"""
bl_idname = "armature.merge"
bl_label = "Merge_Rigs"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return bpy.context.object.type == 'ARMATURE'
def execute(self, context):
target_obj = context.object
if target_obj.type != 'ARMATURE':
return {"CANCELLED"}
target_bones = set([bone.name for bone in target_obj.data.bones])
constraints = []
childrens = {}
#Store children and constraints
bpy.ops.object.mode_set(mode = 'POSE')
for obj in bpy.context.selected_objects:
if obj.type != 'ARMATURE':
continue
if obj == target_obj:
continue
#create a set of all the similiar bones in all the rigs
obj_bones = set([bone.name for bone in obj.data.bones])
shared_bones = target_bones.intersection(obj_bones)
#find all the constraints and children
for bone in obj.pose.bones:
#store all the constraints
for con in bone.constraints:
if not hasattr(con, 'subtarget'):
continue
if con.target == obj and con.subtarget in shared_bones:
constraints.append(constraint_dup(bone, con))
if bone.name in shared_bones:
for child in bone.children:
if child.name in childrens:
continue
childrens.update({child.name : bone.name})
#remove shared bones
bpy.ops.object.mode_set(mode = 'EDIT')
for obj in bpy.context.selected_objects:
if obj.type != 'ARMATURE':
continue
if obj == target_obj:
continue
for bone in shared_bones:
if bone not in obj.data.edit_bones:
continue
obj.data.edit_bones.remove(obj.data.edit_bones[bone])
bpy.ops.object.mode_set(mode = 'POSE')
bpy.ops.object.join()
#restore constraints
for con_dup in constraints:
if con_dup.bone in target_bones:
continue
if con_dup.bone not in target_obj.pose.bones:
continue
#print('constraint on ',con_dup.bone, con_dup.name)
posebone = target_obj.pose.bones[con_dup.bone]
if con_dup.name not in posebone.constraints:
continue
con = posebone.constraints[con_dup.name]
con.target = target_obj
con.subtarget = con_dup.subtarget
#reparent all child bones
bpy.ops.object.mode_set(mode = 'EDIT')
for child, parent in childrens.items():
target_obj.data.edit_bones[child].parent = target_obj.data.edit_bones[parent]
return {"FINISHED"}
classes = (MergeRigs,BboneWidgets, ChainControls)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
# bpy.utils.register_class(BboneWidgets)
# bpy.utils.register_class(ChainControls)
# bpy.utils.register_class(RiggerToolBox_PT_Panel)
def unregister():
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)
# bpy.utils.unregister_class(BboneWidgets)
# bpy.utils.unregister_class(ChainControls)
# bpy.utils.unregister_class(RiggerToolBox_PT_Panel)
if __name__ == "__main__":
register()