2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
# ***** 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
class GizmoSizeUp(bpy.types.Operator):
"""Share keyframes between all the selected objects and bones"""
bl_idname = "view3d.gizmo_size_up"
bl_label = "Gizmo_Size_Up"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
context.preferences.view.gizmo_size += context.scene.animtoolbox.gizmo_size
return {'PASS_THROUGH'}
class GizmoSizeDown(bpy.types.Operator):
"""Share keyframes between all the selected objects and bones"""
bl_idname = "view3d.gizmo_size_down"
bl_label = "Gizmo_Size_Down"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
context.preferences.view.gizmo_size -= context.scene.animtoolbox.gizmo_size
return {'PASS_THROUGH'}
########################################################################################################################
def clear_isolate_pose_mode(scene):
if not len(scene.animtoolbox.isolated):
return
for obj in scene.animtoolbox.isolated:
if not obj.hidden:
continue
obj.hidden.hide_set(False)
scene.animtoolbox.isolated.clear()
scene.animtoolbox.active_obj = None
def isolate_pose_mode(scene):
context = bpy.context
#return when going out of isolate pose or when active object is not in pose mode
if not scene.animtoolbox.isolate_pose_mode or context.active_object.mode != 'POSE':
clear_isolate_pose_mode(scene)
return
#handler continue only if the active object is None otherwise it collects all armature objects
if scene.animtoolbox.active_obj is None:
scene.animtoolbox.active_obj = context.active_object
else:
return
isolated = scene.animtoolbox.isolated
for obj in context.view_layer.objects:
if obj.type != 'ARMATURE':
continue
if obj.hide_get():
continue
rig = isolated.add()
if obj.mode == 'POSE':
rig.selected = obj
else:
rig.hidden = obj
obj.hide_set(True)
class IsolatePoseMode(bpy.types.Operator):
"""Isolates armatures during pose mode"""
bl_idname = "anim.isolate_pose_mode"
bl_label = "Isolate Pose Mode"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
# If the modal is already running, then don't run it the second time
scene = context.scene
if scene.animtoolbox.isolate_pose_mode:
if isolate_pose_mode in bpy.app.handlers.depsgraph_update_pre:
clear_isolate_pose_mode(scene)
bpy.app.handlers.depsgraph_update_pre.remove(isolate_pose_mode)
scene.animtoolbox.isolate_pose_mode = False
return {'FINISHED'}
scene.animtoolbox.isolate_pose_mode = True
isolate_pose_mode(scene)
if isolate_pose_mode not in bpy.app.handlers.depsgraph_update_pre:
bpy.app.handlers.depsgraph_update_pre.append(isolate_pose_mode)
return {'FINISHED'}
class SwitchBoneCollectionsVisibility(bpy.types.Operator):
"""Turn all bone collections visible and then press again to switch back"""
bl_idname = "anim.switch_collections_visibility"
bl_label = "Bone Collections Visibility"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return bpy.app.version >= (4, 0, 0)
def execute(self, context):
obj = context.object
if obj.type != 'ARMATURE':
return {'CANCELLED'}
if not obj.animation_data:
self.report({'INFO'}, 'No animation is available')
return {'CANCELLED'}
if not obj.animation_data.action:
self.report({'INFO'}, 'No animation is available')
return {'CANCELLED'}
collections = obj.data.collections
if not len(collections):
self.report({'INFO'}, 'No collections are available')
return {'CANCELLED'}
#check if there are collections that are marked with
tagged_col = ['atb' in col.keys() for col in collections]
atb_ui = context.scene.animtoolbox
if any(tagged_col) and atb_ui.col_vis:
#collections are already marked so return to previous collection visibilty
for col in collections:
if 'atb' in col.keys():
col.is_visible = col['atb']
del col['atb']
atb_ui.col_vis = False
else:
#Mark visible collections and turn collections with animated bones on
animated_bones = set()
start = 'pose.bones["'
end = '"]'
#get all the animated bones from the fcurves
for fcu in obj.animation_data.action.fcurves:
start_index = fcu.data_path.find(start)
end_index = fcu.data_path.find(end)
#if it's not a posebone fcurve then skip
if start_index == -1 or end_index == -1:
continue
animated_bones.add(fcu.data_path[start_index + len(start):end_index])
#check if the collecetion that is turned off has animated bones
find_anim = []
for col in collections:
for bone in col.bones:
if bone.name in animated_bones and not col.is_visible:
# print(bone.name, 'in ', col.name)
find_anim.append(col)
break
if not find_anim:
self.report({'INFO'}, 'No collections with animated bones and no visibility are found')
return {'CANCELLED'}
#Turn on collections without visiblity
for col in collections:
if col in find_anim:
#tag visibility
col['atb'] = col.is_visible
col.is_visible = True
atb_ui.col_vis = True
return {'FINISHED'}
classes = (GizmoSizeUp, GizmoSizeDown, IsolatePoseMode, SwitchBoneCollectionsVisibility)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
def unregister():
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,549 @@
# ***** 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()
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+527
View File
@@ -0,0 +1,527 @@
# ***** 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 *****
bl_info = {
"name": "AnimToolBox",
"author": "Tal Hershkovich",
"version" : (0, 0, 7, 3),
"blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel",
"description": "A set of animation tools",
"wiki_url": "",
"category": "Animation"}
if "bpy" in locals():
import importlib
if "Rigger_Toolbox" in locals():
importlib.reload(Rigger_Toolbox)
if "TempCtrls" in locals():
importlib.reload(TempCtrls)
if "Tools" in locals():
importlib.reload(Tools)
if "Display" in locals():
importlib.reload(Display)
if "emp" in locals():
importlib.reload(emp)
if "multikey" in locals():
importlib.reload(multikey)
if "Rigger_Toolbox" in locals():
importlib.reload(Rigger_Toolbox)
if "ui" in locals():
importlib.reload(ui)
if "addon_updater_ops" in locals():
importlib.reload(addon_updater_ops)
import bpy
from . import addon_updater_ops
from . import TempCtrls
from . import Rigger_Toolbox
from . import Tools
from . import Display
from . import emp
from . import ui
from . import multikey
from . import Rigger_Toolbox
from pathlib import Path
from bpy.utils import register_class
from bpy.utils import unregister_class
from bpy.app.handlers import persistent
import os
class TempCtrlsItems(bpy.types.PropertyGroup):
#located at context.scene.btc.ctrl_items
controlled: bpy.props.PointerProperty(name = "controlled object", description = "rigs and objects that are being controlled", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
controller: bpy.props.PointerProperty(name = "controller object", description = "rigs and objects that are controling", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
class TempCtrlsSceneSettings(bpy.types.PropertyGroup):
#located at context.scene.btc
root: bpy.props.BoolProperty(name = "Root Empty", description = "Add a root to the empties ", default = False, override = {'LIBRARY_OVERRIDABLE'}, update = TempCtrls.root_prop)
root_bone: bpy.props.StringProperty(name = "Root bone", description = "Root empty as a root bone ", override = {'LIBRARY_OVERRIDABLE'}, update = TempCtrls.root_update)
root_object: bpy.props.PointerProperty(name = "Root object", description = "Root empty as a root object ", update = TempCtrls.root_update, type = bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
ctrl_type: bpy.props.EnumProperty(name = 'Controllers', description="Select empties or a bone with a new rig to bake to", items = [('BONE', 'Bone','Bake to bones','BONE_DATA', 0), ('EMPTY', 'Empty', 'Bake to empties', 'EMPTY_ARROWS', 1)])
ctrl_items: bpy.props.CollectionProperty(type = TempCtrlsItems, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
bake_range_type: bpy.props.EnumProperty(name = 'Bake Range', description="Use either scene, actions length or custom frame range", default = 'KEYFRAMES', update= TempCtrls.update_range_type,
items = [('SCENE', 'Scene Range', 'Bake to the scene range'), ('KEYFRAMES', 'Keyframes Range', 'Bake all the keyframes in the layers'), ('CUSTOM', 'Custom', 'Enter a custom frame range')], override = {'LIBRARY_OVERRIDABLE'})
bake_range: bpy.props.IntVectorProperty(name='Frame Range', description='Bake to a custom frame range', size = 2, update= TempCtrls.update_bake_range)
bake_layers: bpy.props.BoolProperty(name = "Bake Layers", description = "Use keyframes from all the layers to include in the bake", default = False)
target: bpy.props.EnumProperty(name = 'Affect', description="Cleanup created constraints and empties", default = 1,
items = [('ALL', 'All Ctrl Rigs','Bake to all Ctrl Rigs', 0),
('SELECTED', 'Selected Chains','Bake to only selected chain controlls', 1),
('RELATIVE', 'Relative Ctrls Rig','Bake to the Relative Control rigs', 2)])
# ('CONSTRAINTS', 'Constraints', 'Clean all the bone constraints', 3),
# ('CONTROLLERS', 'Controllers', 'Remove all the baked empties', 4)])
selection: bpy.props.EnumProperty(name = 'Select', description="Select all controls, original bones or their relative", default = 'CONTROLLERS',
items = [('RELATIVE_CTRLS', 'Relative Ctrls','Select the Relative controller to your current selection', 0),
('RELATIVE_CONSTRAINED', 'Relative Constrained','Select the Relative original constrained bone to your current selection', 1),
('CONTROLLERS', 'All Ctrls', 'Select all the controller bones or empties', 2), ('CONSTRAINED', 'All Constrained', 'Select all the original constrained bones', 3)])
#smartbake setting
linksettings: bpy.props.BoolProperty(name = "Link Settings", description = "Link Settings", default = False, override = {'LIBRARY_OVERRIDABLE'})
bakesettings: bpy.props.BoolProperty(name = "bake settings", description = "bake settings", default = False, override = {'LIBRARY_OVERRIDABLE'})
cleansettings: bpy.props.BoolProperty(name = "clean settings", description = "clean settings", default = False, override = {'LIBRARY_OVERRIDABLE'})
smartbake: bpy.props.BoolProperty(name = "Smart Bake", description = "Keep Original Frame count", default = True, override = {'LIBRARY_OVERRIDABLE'})
inbetween_keyframes: bpy.props.IntProperty(name = "Inbetween Keyframes", description = "Add inbetween keyframes", default = 0, override = {'LIBRARY_OVERRIDABLE'})
from_origin: bpy.props.BoolProperty(name = "From Origin", description = "Use Keyframes from Original Bone", default = True, override = {'LIBRARY_OVERRIDABLE'})
from_ctrl: bpy.props.BoolProperty(name = "From Controller", description = "Use Keyframes from Controller Bone", default = False, override = {'LIBRARY_OVERRIDABLE'})
clean_ctrls: bpy.props.BoolProperty(name = "Remove Ctrls", description = "Remove Controls", default = True, override = {'LIBRARY_OVERRIDABLE'})
clean_constraints: bpy.props.BoolProperty(name = "Remove Constraints", description = "Remove Constraints", default = True, override = {'LIBRARY_OVERRIDABLE'})
rebake_to_org: bpy.props.BoolProperty(name = "ReBake connections to original bones", description = "ReBake ctrls from connected chains current anim to the original bones", default = False, override = {'LIBRARY_OVERRIDABLE'})
link_to: bpy.props.EnumProperty(name = 'Link to Chain', description="Link to begining of an active chain or the tip of the chain", default = 1,
items = [('BASE', 'Base','Link to the base of the active chain', 0), ('TIP', 'Tip', 'Link to the tip of the chain', 1)])
# link_from: bpy.props.EnumProperty(name = 'Link to Chain', description="Link to begining of an active chain or the tip of the chain", default = 0,
# items = [('BASE', 'Base','Link to the base of the active chain', 0), ('TIP', 'Tip', 'Link to the tip of the chain', 1)])
shape_size: bpy.props.FloatProperty(name='Size', description="Multiple factor for the shape size of the temp controls", update = TempCtrls.tempctrl_shapesize, min = 0.001, default = 1.5, override = {'LIBRARY_OVERRIDABLE'})#
shape_type: bpy.props.EnumProperty(name = 'Shape Type', description="Display type for the controls", items = TempCtrls.ctrl_shape_items, update = TempCtrls.tempctrl_shape_type)
color_set: bpy.props.EnumProperty(name="Bone Color Set", description="Choose a bone color set", items = TempCtrls.get_bone_color_sets, update = TempCtrls.update_bone_color, default = 9)
add_ik_ctrl: bpy.props.BoolProperty(name = 'Add an Extra IK Ctrl Bone', description = "Adds an extra bone ctrl as the ik ctrl", default = False, update = TempCtrls.add_ik_prop)
pole_target: bpy.props.BoolProperty(name = 'Add Pole Target', description = "Adding Pole Target to the IK Chain", default = True, update = TempCtrls.pole_prop)
pole_offset: bpy.props.FloatProperty(name="Offset", description="Offset the bone in the axis direction", default=1.0, update = TempCtrls.pole_offset)
child: bpy.props.BoolProperty(name = 'Add extra child Ctrls', description = "Add an child control for an overlay control", default = False, update = TempCtrls.child_prop)
orientation: bpy.props.BoolProperty(name = 'Use World Space Orientation', description = "Orient the bones to world space instead of to the original bones", default = True)
# enabled: bpy.props.BoolProperty(name = 'Switch On / Off', description = "Enabling and Disabling Temp Ctrls influence", default = True)
class TempCtrlsBoneSettings(bpy.types.PropertyGroup):
#located at obj.pose.bones[##].btc
root: bpy.props.BoolProperty(name = "Root Bone", description = "Bone is marked as the root bone", default = False, override = {'LIBRARY_OVERRIDABLE'})
child: bpy.props.BoolProperty(name = "Child Bone", description = "Bone is marked as a child bone inside the setup", default = False, override = {'LIBRARY_OVERRIDABLE'})
org_id: bpy.props.IntProperty(name = "Originate ID", description = "ID number of the bone the ctrl originates from", override = {'LIBRARY_OVERRIDABLE'})
setup_id: bpy.props.IntProperty(name = "Setup ID", description = "ID number of the current chain setup", override = {'LIBRARY_OVERRIDABLE'})
setup: bpy.props.EnumProperty(name = 'Setup Type', description="Describes what kind of setup the bone is part of", override = {'LIBRARY_OVERRIDABLE'},
items = [('NONE', 'No Setup','No Setup Applied', 0),
('WORLDSPACE', 'World Space Ctrl','World Space Ctrl setup', 1),
('TEMPFK', 'Temporary FK setup','Temporary FK chain setup', 2),
('TEMPFK_FLIP', 'Temporary flipped FK setup','Temporary flipped FK chain setup', 3),
('TEMPIK', 'Temporary IK setup','Temporary IK setup', 4),
('POLE', 'Temporary IK Pole setup','Temporary IK Pole', 5),
('PARENTCTRL', 'Parent Ctrl from cursor setup','Parent Ctrl from cursor setup', 6),
('ROOT', 'Root', 'Root Ctrl for all the setups', 7),
('EMPTY', 'Root', 'Root Ctrl for all the setups', 8),
('TRACK_TO', 'Track To','World Space Track to Ctrl setup', 9),
('TRACK_TO_EMPTY', 'Track To Empty','World Space Track to Empty Ctrl setup', 10)])
#using org mostly to decide if it needs a custom shape
org: bpy.props.EnumProperty(name = 'Org Type', description="Describes what if the function of the bone", override = {'LIBRARY_OVERRIDABLE'},
items = [('CTRL', 'Controller Bone','Controller Bone', 0),
('ORG', 'Original Bone','Original Bone', 1),
('MCH', 'Mechanical Bone','Mechanical Bone', 2),
('NONE', 'Nothing applied','Nothing applied', 3)])
shape: bpy.props.BoolProperty(name = "Apply shape", description = "Mark if the bone needs a shape", default = False, override = {'LIBRARY_OVERRIDABLE'})
class TempCtrlsObjectSetups(bpy.types.PropertyGroup):
#located at obj.animtoolbox.ctrl_setups
#name: using string of the id
setup: bpy.props.EnumProperty(name = 'Setup Type', description="Describes what kind of setup the bone is part of",
items = [('NONE', 'No Setup','No Setup Applied', 0),
('WORLDSPACE', 'World Space Ctrl','World Space Ctrl setup', 1),
('TEMPFK', 'Temporary FK setup','Temporary FK chain setup', 2),
('TEMPFK_FLIP', 'Temporary flipped FK setup','Temporary flipped FK chain setup', 3),
('TEMPIK', 'Temporary IK setup','Temporary IK setup', 4),
('PARENTCTRL', 'Parent Ctrl from cursor setup','Parent Ctrl from cursor setup', 5),
('ROOT', 'Root', 'Root Ctrl for all the setups', 6),
('EMPTY', 'Root', 'Root Ctrl for all the setups', 8),
('TRACK_TO', 'Track To','World Space Track to Ctrl setup', 9),
('TRACK_TO_EMPTY', 'Track To Empty','World Space Track to Empty Ctrl setup', 10),
])
class MultikeyProperties(bpy.types.PropertyGroup):
selectedbones: bpy.props.BoolProperty(name="Selected Bones", description="Affect only selected bones", default=True, options={'HIDDEN'})
handletype: bpy.props.BoolProperty(name="Keep handle types", description="Keep handle types", default=False, options={'HIDDEN'})
scale: bpy.props.FloatProperty(name="Scale Factor", description="Scale percentage of the average value", default=1.0, update = multikey.scale_value)
randomness: bpy.props.FloatProperty(name="Randomness", description="Random Threshold of keyframes", default=0.1, min=0.0, max = 1.0, update = multikey.random_value)
class AnimToolBoxObjectSettings(bpy.types.PropertyGroup):
controlled: bpy.props.PointerProperty(name = 'Controlled Rig', description="Adding the rig object that is being controlled by the current object", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
controller: bpy.props.PointerProperty(name = 'Controller Rig', description="Adding the rig object that is used as the temp control object", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
ctrl_setups: bpy.props.CollectionProperty(type = TempCtrlsObjectSetups, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
ctrls_enabled: bpy.props.BoolProperty(name = 'Temp Ctrls Switch', description = "Enabling and Disabling Temp Ctrls influence", default = True)
# influence: bpy.props.FloatProperty(name = "Influence Slider for the Temp Ctrls", description = "Switching the influence slider for the temp ctrls", default = 1, min = 0.0, max = 1.0)
#Used for Bake to Empties
# org_id: bpy.props.IntProperty(name = "Originate ID", description = "ID number of the bone the ctrl originates from", override = {'LIBRARY_OVERRIDABLE'})
setup_id: bpy.props.IntProperty(name = "Setup ID", description = "ID number of the current chain setup", override = {'LIBRARY_OVERRIDABLE'})
root: bpy.props.BoolProperty(name = "Root Empty", description = "Empty is marked as the root ctrl", default = False, override = {'LIBRARY_OVERRIDABLE'})
child: bpy.props.BoolProperty(name = "Child Empty", description = "Empty is marked as a child bone inside the setup", default = False, override = {'LIBRARY_OVERRIDABLE'})
keyframes_offset: bpy.props.FloatProperty(name = "Keyframes Offset", description = "Interactive slider to offset keyframes back and forth ", default = 0)
class IsolatedRigs(bpy.types.PropertyGroup):
hidden: bpy.props.PointerProperty(name = "Hidden Rigs", description = "List of Rigs that are hidden during pose mode isolation", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
selected: bpy.props.PointerProperty(name = "Selected Rigs", description = "List of Rigs that are hidden during pose mode isolation", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
class AnimToolBoxUILayout(bpy.types.PropertyGroup):
'''Layout properties for the UI'''
quick_menu: bpy.props.BoolProperty(name = "Use Quick Menu", description = "Opens header menu with only icon", default = False)
copy_paste_matrix: bpy.props.BoolProperty(name = "Copy Matrix Menu", description = "Opens the menu for copy paste matrices", default = True)
copy_paste_world: bpy.props.BoolProperty(name = "Copy Paste World Matrix", description = "Copy and Paste the World Matrix", default = False, update = Tools.copy_paste_world_update)
copy_paste_relative: bpy.props.BoolProperty(name = "Copy Paste Relative Matrix", description = "Copy and Paste the Matrix relative to the active bone", default = False, update = Tools.copy_paste_relative_update)
Inbetweens: bpy.props.BoolProperty(name = "Blendings/Inbetweens", description = "Opens the menu for Inbetweens", default = True)
gizmo_size: bpy.props.BoolProperty(name = "Gizmo size", description = "Change the Gizmo size using alt +/- hotkeys", default = False)
# temp_ctrls: bpy.props.BoolProperty(name = "Temp Ctrls", description = "Open Temp Ctrls", default = False)
temp_ctrls_switch: bpy.props.BoolProperty(name = "Temp Ctrls Switch", description = "Temp Ctrls Switch", default = True)
temp_ctrls_shapes: bpy.props.BoolProperty(name = "Temp Ctrls Shapes", description = "Temp Ctrls Shapes", default = True)
markers_retimer: bpy.props.BoolProperty(name = "Marker Retimer", description = "Flag when marker retimer turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
relative_cursor: bpy.props.BoolProperty(name = "Relative Cursor Mode", description = "Cursor moves relative to the selection", default = False)
is_dragging: bpy.props.BoolProperty(default = False)
#using Blending sliders in the window manager to avoid undo issues with modal operators
inbetween_worldmatrix: bpy.props.FloatProperty(name='Inbetween World Matrix', description="Adds an inbetween of the World Matrix to the Layer's neighbor keyframes", soft_min = -1, soft_max = 1, default=0.0, update = Tools.add_inbetween_worldmatrix, override = {'LIBRARY_OVERRIDABLE'})
blend_mirror: bpy.props.FloatProperty(name='Blend Mirror', description="Blend into the mirrored pose", soft_min = 0, soft_max = 1, default=0, step = 1, update = Tools.blend_to_mirror, override = {'LIBRARY_OVERRIDABLE'})
multikey: bpy.props.PointerProperty(type = MultikeyProperties, options={'LIBRARY_EDITABLE'}, override = {'LIBRARY_OVERRIDABLE'})
class AnimToolBoxGlobalSettings(bpy.types.PropertyGroup):
#context.scene.animtoolbox
marker_frame_range: bpy.props.BoolProperty(name = "Marker Frame Range", description = "Flag when marker frame range turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
bake_frame_range: bpy.props.BoolProperty(name = "Bake Frame Range", description = "Flag when marker bake range turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
# markers_retimer: bpy.props.BoolProperty(name = "Marker Retimer", description = "Flag when marker retimer turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
keyframes_offset: bpy.props.FloatProperty(name = "Keyframes Offset", description = "Interactive slider to offset keyframes back and forth ", soft_max = 2, soft_min = -2, default = 0, update = Tools.keyframes_offset_slider)
rotation_mode: bpy.props.EnumProperty(name = 'Rotation Mode', description="Describes what kind of setup the bone is part of", override = {'LIBRARY_OVERRIDABLE'},
items = [('QUATERNION', 'Quaternion','Quaternion Rotation Order - No Gimbal Lock', 0),
('XYZ', 'XYZ', 'XYZ Rotation Order', 1), ('XZY', 'XZY','XZY Rotation Order', 2),
('YXZ', 'YXZ','YXZ Rotation Order', 3), ('YZX', 'YZX', 'YZX Rotation Order', 4),
('ZXY', 'ZXY', 'ZXY Rotation Order', 5), ('ZYX', 'ZYX', 'ZYX Rotation Order', 6),
('AXIS_ANGLE', 'AXIS_ANGLE', 'Axis Angle Rotation Order', 7)])
isolate_pose_mode: bpy.props.BoolProperty(name = "Isolate rig in pose mode", description = "Isolates the rig during pose mode, rigs in object mode are hidden", default = False)
active_obj: bpy.props.PointerProperty(name = "Active Object", description = "Current Active Object", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
isolated: bpy.props.CollectionProperty(type = IsolatedRigs, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
#Blendings
inbetweener : bpy.props.FloatProperty(name='Inbetween Keyframe', description="Adds an inbetween Keyframe between the Layer's neighbor keyframes", soft_min = -1, soft_max = 1, default=0.0, options = set(), update = Tools.add_inbetween_key, override = {'LIBRARY_OVERRIDABLE'})
motion_path: bpy.props.BoolProperty(name = "Motion Path", description = "Flag when Motion Path is on", default = False, override = {'LIBRARY_OVERRIDABLE'})
mp_settings: bpy.props.BoolProperty(name = "Motion Path Settings", description = "Open the settings Menu", default = False, override = {'LIBRARY_OVERRIDABLE'})
mp_keyframe_scale: bpy.props.FloatProperty(name = "Scale Selecgted Keyframes Bounding Box", description = "Change the scale of the bounding box around the selected keyframes ", default = 0.1, step = 0.1, precision = 3)
mp_color_before: bpy.props.FloatVectorProperty(name="Motion Path Before Color", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, description="Motion path color before the current frame")
mp_color_after: bpy.props.FloatVectorProperty(name="Motion Path After Color", subtype='COLOR', default=(0.0, 1.0, 0.0), min=0.0, max=1.0, description="Motion path color before the current frame")
mp_infront: bpy.props.BoolProperty(name = "Motion Path In Front", description = "Display motion path in front of all the objects", default = True, override = {'LIBRARY_OVERRIDABLE'})
mp_points: bpy.props.BoolProperty(name = "Motion Path Points", description = "Display motion path points", default = True, override = {'LIBRARY_OVERRIDABLE'})
mp_lines: bpy.props.BoolProperty(name = "Motion Path Lines", description = "Display motion path lines", default = True, override = {'LIBRARY_OVERRIDABLE'})
mp_handles: bpy.props.BoolProperty(name = "Motion Path Handles", description = "Display motion path handles on keyframe selection", default = True, override = {'LIBRARY_OVERRIDABLE'})
mp_display_frames: bpy.props.BoolProperty(name = "Frame Numbers", description = "Display frame numbers on all the keyframes", default = False, override = {'LIBRARY_OVERRIDABLE'})
mp_handle_types: bpy.props.EnumProperty(name = 'Set Keyframe Handle Type', description="Set handle type for selected keyframes", default = 'AUTO', update = emp.update_handle_type_prop,
items = [('FREE', 'Free', 'Free', 'HANDLE_FREE', 0),
('ALIGNED','Aligned', 'Aligned', 'HANDLE_ALIGNED', 1),
('VECTOR', 'Vector', 'Vector', 'HANDLE_VECTOR', 2),
('AUTO','Automatic', 'Automatic', 'HANDLE_AUTO', 3),
('AUTO_CLAMPED','Auto Clamped', 'Auto Clamped', 'HANDLE_AUTOCLAMPED', 4)])
mp_interpolation: bpy.props.EnumProperty(name = 'Set Interpolation', description="Set Keyframe Interpolation", default = 'BEZIER', update = emp.update_interpolation_prop,
items = [('BEZIER', 'Bezier', 'Bezier', 'IPO_BEZIER', 0),
('LINEAR','Linear', 'Linear', 'IPO_LINEAR', 1),
('CONSTANT', 'Constant', 'Constant', 'IPO_CONSTANT', 2)])
mp_frame_range: bpy.props.EnumProperty(name = 'Frame Range', description="Type of Frame Range", default = 'SCENE', #update = emp.mp_frame_range_update,
items = [('KEYS_ALL', 'All_Keys','Use the Scene Frame Length for the Range', 0),
('SCENE', 'Scene','Use the Scene Frame Length for the Range', 1),
('MANUAL', 'Manual','Custom Frame range using numerical input or the markers frame ranger', 2),
('AROUND', 'Around Frames','Show only around the current frame', 3)])
mp_before: bpy.props.IntProperty(name = "Before", description = "Show the frames Before the current frame", min = 0, default = 10)
mp_after: bpy.props.IntProperty(name = "After", description = "Show the frames After the current frame", min = 0, default = 10)
selected_keyframes: bpy.props.StringProperty(name="Selected Keyframes", description="Serialized representation of selected keyframes")
gizmo_size: bpy.props.IntProperty(name = "Add to Gizmo Size", description = "Addition to Gizmo Size", max = 100, min = -100, default = 10)
#Copy/Pase Matrix
range_type: bpy.props.EnumProperty(name = 'Paste to Frames', description="Paste to current frame or a range of frames.", update = Tools.bake_range_type,
items = [('CURRENT', 'Current Frame','Paste Matrix to only current frame', 0),
('SELECTED', 'Selected Keyframe','Paste Matrix to only selected keyframes', 1),
('RANGE', 'Frame Range','Paste Matrix to a Frame Range', 2)])
bake_frame_start: bpy.props.IntProperty(name = "Bake Frame Start", description = "Define the start frame to paste the matrix", min = 0, update = Tools.bake_frame_start_limit)
bake_frame_end: bpy.props.IntProperty(name = "Bake Frame End", description = "Define the end frame to paste the matrix", min = 0, update = Tools.bake_frame_end_limit)
filter_location: bpy.props.BoolVectorProperty(name="Location", description="Filter Location properties", default=(False, False, False), size = 3, options={'HIDDEN'}, update = Tools.filter_name_update)
filter_rotation: bpy.props.BoolVectorProperty(name="Rotation", description="Filter Rotation properties", default=(False, False, False, False), size = 4, options={'HIDDEN'}, update = Tools.filter_name_update)
filter_scale: bpy.props.BoolVectorProperty(name="Scale", description="Filter Scale properties", default=(False, False, False), size = 3, options={'HIDDEN'}, update = Tools.filter_name_update)
#The name displayed on the filter button
filter_name: bpy.props.StringProperty(name="Filter Name", description="Change the name of the button while chaging the filter options", default= "", options={'HIDDEN'})
filter_custom_props: bpy.props.BoolProperty(name = "Filter Custom Properties", description = "Filter custom properties", default = False)
filter_keyframes: bpy.props.BoolProperty(name = "Filter Aelected Keyframes", description = "Filter selected keyframes for specific tools", default = False)
col_vis: bpy.props.BoolProperty(name = "Animated collections visibility", description = "Display if animated collections are turned on or off", default = False)
@addon_updater_ops.make_annotations
class AnimToolBoxPreferences(bpy.types.AddonPreferences):
# this must match the addon name, use '__package__'
# when defining this in a submodule of a python package.
bl_idname = __package__
category: bpy.props.StringProperty(
name="Tab Category",
description="Choose a name for the category of the panel",
default="Animation",
update=ui.update_panel
)
quick_menu: bpy.props.BoolProperty(name = "Use Quick Menu", description = "Opens header menu with only icon", default = False)
riggertoolbox: bpy.props.BoolProperty(name = "RiggerToolBox", description = "Include RiggerToolbox (experimental)", default = False, update = ui.add_riggertoolbox)
multikey: bpy.props.BoolProperty(name = "Multikey", description = "Include Multikey for adju\sting multiply keyframes", default = False, update = ui.add_multikey)
#Temp Ctrls properties
in_front: bpy.props.BoolProperty(name = "Always In Front", description = "Set Temp Ctrls to be always in front", default = True)
clear_setup : bpy.props.BoolProperty(name = "Clear Selection Before Creating New Temp Ctrls", description = "Clear old setup when adding Temp ctrls to an existing chain", default = False)
#Editable motion path
keyframes_range: bpy.props.IntProperty(name = "Keyframe Range", description = "The range of distance from the keyframes while hovering over them", min = 5, max = 100, default = 15)
# addon updater preferences from `__init__`, be sure to copy all of them
auto_check_update: bpy.props.BoolProperty(
name = "Auto-check for Update",
description = "If enabled, auto-check for updates using an interval",
default = True,
)
updater_interval_months: bpy.props.IntProperty(
name='Months',
description = "Number of months between checking for updates",
default=0,
min=0
)
updater_interval_days: bpy.props.IntProperty(
name='Days',
description = "Number of days between checking for updates",
default=7,
min=0,
)
updater_interval_hours: bpy.props.IntProperty(
name='Hours',
description = "Number of hours between checking for updates",
default=0,
min=0,
max=23
)
updater_interval_minutes: bpy.props.IntProperty(
name='Minutes',
description = "Number of minutes between checking for updates",
default=0,
min=0,
max=59
)
#Draw the UI in the preferences
def draw(self, context):
layout = self.layout
addon_updater_ops.update_settings_ui(self, context)
row = layout.row()
col = row.column()
col.label(text="Tab Category:")
col.prop(self, "category", text="")
layout.separator()
col = layout.column()
col.prop(self, "quick_menu", text="Use Quick Icons Menu")
layout.separator()
box = layout.box()
row = box.row()
row.label(text = 'Temp Ctrls: ')
row = box.row()
row.prop(self, 'clear_setup')
row.prop(self, 'in_front')
layout.separator()
col = layout.column()
col.label(text = 'Include Extras: ')
row = layout.row()
row.prop(self, "multikey", text="Multikey - Edit Multiply keyframes")
row.prop(self, "riggertoolbox", text="RiggerToolBox (Experimental)")
@persistent
def loadanimtoolbox_pre(self, context):
scene = bpy.context.scene
dns = bpy.app.driver_namespace
if scene.animtoolbox.bake_frame_range:
scene.animtoolbox.bake_frame_range = False
if scene.animtoolbox.motion_path:
scene.animtoolbox.motion_path = False
if 'mp_dh' in dns:
bpy.types.SpaceView3D.draw_handler_remove(dns['mp_dh'], 'WINDOW')
bpy.app.driver_namespace.pop('mp_dh')
if 'markers_retimer_dh' in dns:
bpy.types.SpaceView3D.draw_handler_remove(dns['markers_retimer_dh'], 'WINDOW')
bpy.app.driver_namespace.pop('markers_retimer_dh')
#remove the motion path app handler if it's still inside
if emp.mp_value_update in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(emp.mp_value_update)
if emp.mp_frame_change in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.remove(emp.mp_frame_change)
if emp.mp_undo_update in bpy.app.handlers.undo_pre:
bpy.app.handlers.undo_pre.remove(emp.mp_undo_update)
@persistent
def loadanimtoolbox_post(self, context):
scene = bpy.context.scene
dns = bpy.app.driver_namespace
if scene.animtoolbox.isolate_pose_mode:
if Display.isolate_pose_mode not in bpy.app.handlers.depsgraph_update_pre:
bpy.app.handlers.depsgraph_update_pre.append(Display.isolate_pose_mode)
if scene.animtoolbox.motion_path:
scene.animtoolbox.motion_path = False
if 'mp_dh' in dns:
bpy.types.SpaceView3D.draw_handler_remove(dns['mp_dh'], 'WINDOW')
bpy.app.driver_namespace.pop('mp_dh')
Tools.selection_order(self, context)
classes = (TempCtrlsItems, TempCtrlsObjectSetups, TempCtrlsSceneSettings,TempCtrlsBoneSettings, MultikeyProperties, IsolatedRigs,
AnimToolBoxObjectSettings, AnimToolBoxUILayout, AnimToolBoxGlobalSettings) + ui.classes
addon_keymaps = []
def register():
# Note that preview collections returned by bpy.utils.previews
# are regular py objects - you can use them to store custom data.
addon_updater_ops.register(bl_info)
register_class(AnimToolBoxPreferences)
addon_updater_ops.make_annotations(AnimToolBoxPreferences) # to avoid blender 2.8 warnings
TempCtrls.register()
Tools.register()
Display.register()
emp.register()
ui.register_custom_icon()
for cls in classes:
# print(cls)
register_class(cls)
bpy.types.Scene.btc = bpy.props.PointerProperty(type = TempCtrlsSceneSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.PoseBone.btc = bpy.props.PointerProperty(type = TempCtrlsBoneSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.Object.animtoolbox = bpy.props.PointerProperty(type = AnimToolBoxObjectSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.Scene.animtoolbox = bpy.props.PointerProperty(type = AnimToolBoxGlobalSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.WindowManager.atb_ui = bpy.props.PointerProperty(type = AnimToolBoxUILayout, override = {'LIBRARY_OVERRIDABLE'})
ui.update_panel(None, bpy.context)
ui.add_multikey(None, bpy.context)
ui.add_riggertoolbox(None, bpy.context)
if loadanimtoolbox_pre not in bpy.app.handlers.load_pre:
bpy.app.handlers.load_pre.append(loadanimtoolbox_pre)
if loadanimtoolbox_post not in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.append(loadanimtoolbox_post)
if Tools.selection_order not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(Tools.selection_order)
#Make sure TAB hotkey in the NLA goes into full stack mode
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
km = kc.keymaps.new(name= '3D View', space_type= 'VIEW_3D')
if 'view3d.gizmo_size_up' not in km.keymap_items:
kmi = km.keymap_items.new('view3d.gizmo_size_up', type= 'NUMPAD_PLUS', value= 'PRESS', alt = True, repeat = True)
addon_keymaps.append((km, kmi))
if 'view3d.gizmo_size_down' not in km.keymap_items:
kmi = km.keymap_items.new('view3d.gizmo_size_down', type= 'NUMPAD_MINUS', value= 'PRESS', alt = True, repeat = True)
addon_keymaps.append((km, kmi))
#Add Tools to the Toolbar
bpy.utils.register_tool(ui.KeyframeOffsetTool, separator=True)
#Add tools to the menu
bpy.types.VIEW3D_MT_editor_menus.append(ui.draw_menu)
def unregister():
for pcoll in ui.preview_collections.values():
bpy.utils.previews.remove(pcoll)
ui.preview_collections.clear()
#addon_updater_ops.unregister()
addon_updater_ops.unregister()
unregister_class(AnimToolBoxPreferences)
TempCtrls.unregister()
# Rigger_Toolbox.unregister()
Tools.unregister()
Display.unregister()
emp.unregister()
ui.add_multikey(None, bpy.context)
ui.add_riggertoolbox(None, bpy.context)
for cls in classes:
unregister_class(cls)
bpy.utils.unregister_tool(ui.KeyframeOffsetTool)
#Remove the header menu ui
bpy.types.VIEW3D_MT_editor_menus.remove(ui.draw_menu)
del bpy.types.Scene.btc
# del bpy.types.Bone.btc
del bpy.types.Object.animtoolbox
del bpy.types.Scene.animtoolbox
if hasattr(bpy.types.Object, 'keyframes_offset'):
del bpy.types.Object.keyframes_offset
if hasattr(bpy.types.PoseBone, 'keyframes_offset'):
del bpy.types.PoseBone.keyframes_offset
if loadanimtoolbox_pre in bpy.app.handlers.load_pre:
bpy.app.handlers.load_pre.remove(loadanimtoolbox_pre)
if loadanimtoolbox_post in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(loadanimtoolbox_post)
if Tools.selection_order in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(Tools.selection_order)
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
if __name__ == "__main__":
register()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
{
"last_check": "2025-06-19 15:30:39.178174",
"backup_date": "June-19-2025",
"update_ready": false,
"ignore": false,
"just_restored": false,
"just_updated": false,
"version_text": {}
}
@@ -0,0 +1,189 @@
# ***** 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
class GizmoSizeUp(bpy.types.Operator):
"""Share keyframes between all the selected objects and bones"""
bl_idname = "view3d.gizmo_size_up"
bl_label = "Gizmo_Size_Up"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
context.preferences.view.gizmo_size += context.scene.animtoolbox.gizmo_size
return {'PASS_THROUGH'}
class GizmoSizeDown(bpy.types.Operator):
"""Share keyframes between all the selected objects and bones"""
bl_idname = "view3d.gizmo_size_down"
bl_label = "Gizmo_Size_Down"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
context.preferences.view.gizmo_size -= context.scene.animtoolbox.gizmo_size
return {'PASS_THROUGH'}
########################################################################################################################
def clear_isolate_pose_mode(scene):
if not len(scene.animtoolbox.isolated):
return
for obj in scene.animtoolbox.isolated:
if not obj.hidden:
continue
obj.hidden.hide_set(False)
scene.animtoolbox.isolated.clear()
scene.animtoolbox.active_obj = None
def isolate_pose_mode(scene):
context = bpy.context
#return when going out of isolate pose or when active object is not in pose mode
if not scene.animtoolbox.isolate_pose_mode or context.active_object.mode != 'POSE':
clear_isolate_pose_mode(scene)
return
#handler continue only if the active object is None otherwise it collects all armature objects
if scene.animtoolbox.active_obj is None:
scene.animtoolbox.active_obj = context.active_object
else:
return
isolated = scene.animtoolbox.isolated
for obj in context.view_layer.objects:
if obj.type != 'ARMATURE':
continue
if obj.hide_get():
continue
rig = isolated.add()
if obj.mode == 'POSE':
rig.selected = obj
else:
rig.hidden = obj
obj.hide_set(True)
class IsolatePoseMode(bpy.types.Operator):
"""Isolates armatures during pose mode"""
bl_idname = "anim.isolate_pose_mode"
bl_label = "Isolate Pose Mode"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
# If the modal is already running, then don't run it the second time
scene = context.scene
if scene.animtoolbox.isolate_pose_mode:
if isolate_pose_mode in bpy.app.handlers.depsgraph_update_pre:
clear_isolate_pose_mode(scene)
bpy.app.handlers.depsgraph_update_pre.remove(isolate_pose_mode)
scene.animtoolbox.isolate_pose_mode = False
return {'FINISHED'}
scene.animtoolbox.isolate_pose_mode = True
isolate_pose_mode(scene)
if isolate_pose_mode not in bpy.app.handlers.depsgraph_update_pre:
bpy.app.handlers.depsgraph_update_pre.append(isolate_pose_mode)
return {'FINISHED'}
class SwitchBoneCollectionsVisibility(bpy.types.Operator):
"""Turn all bone collections visible and then press again to switch back"""
bl_idname = "anim.switch_collections_visibility"
bl_label = "Bone Collections Visibility"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return bpy.app.version >= (4, 0, 0)
def execute(self, context):
obj = context.object
if obj.type != 'ARMATURE':
return {'CANCELLED'}
if not obj.animation_data:
self.report({'INFO'}, 'No animation is available')
return {'CANCELLED'}
if not obj.animation_data.action:
self.report({'INFO'}, 'No animation is available')
return {'CANCELLED'}
collections = obj.data.collections
if not len(collections):
self.report({'INFO'}, 'No collections are available')
return {'CANCELLED'}
#check if there are collections that are marked with
tagged_col = ['atb' in col.keys() for col in collections]
atb_ui = context.scene.animtoolbox
if any(tagged_col) and atb_ui.col_vis:
#collections are already marked so return to previous collection visibilty
for col in collections:
if 'atb' in col.keys():
col.is_visible = col['atb']
del col['atb']
atb_ui.col_vis = False
else:
#Mark visible collections and turn collections with animated bones on
animated_bones = set()
start = 'pose.bones["'
end = '"]'
#get all the animated bones from the fcurves
for fcu in obj.animation_data.action.fcurves:
start_index = fcu.data_path.find(start)
end_index = fcu.data_path.find(end)
#if it's not a posebone fcurve then skip
if start_index == -1 or end_index == -1:
continue
animated_bones.add(fcu.data_path[start_index + len(start):end_index])
#check if the collecetion that is turned off has animated bones
find_anim = []
for col in collections:
for bone in col.bones:
if bone.name in animated_bones and not col.is_visible:
# print(bone.name, 'in ', col.name)
find_anim.append(col)
break
if not find_anim:
self.report({'INFO'}, 'No collections with animated bones and no visibility are found')
return {'CANCELLED'}
#Turn on collections without visiblity
for col in collections:
if col in find_anim:
#tag visibility
col['atb'] = col.is_visible
col.is_visible = True
atb_ui.col_vis = True
return {'FINISHED'}
classes = (GizmoSizeUp, GizmoSizeDown, IsolatePoseMode, SwitchBoneCollectionsVisibility)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
def unregister():
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)
@@ -0,0 +1,549 @@
# ***** 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()
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,527 @@
# ***** 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 *****
bl_info = {
"name": "AnimToolBox",
"author": "Tal Hershkovich",
"version" : (0, 0, 7, 1),
"blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel",
"description": "A set of animation tools",
"wiki_url": "",
"category": "Animation"}
if "bpy" in locals():
import importlib
if "Rigger_Toolbox" in locals():
importlib.reload(Rigger_Toolbox)
if "TempCtrls" in locals():
importlib.reload(TempCtrls)
if "Tools" in locals():
importlib.reload(Tools)
if "Display" in locals():
importlib.reload(Display)
if "emp" in locals():
importlib.reload(emp)
if "multikey" in locals():
importlib.reload(multikey)
if "Rigger_Toolbox" in locals():
importlib.reload(Rigger_Toolbox)
if "ui" in locals():
importlib.reload(ui)
if "addon_updater_ops" in locals():
importlib.reload(addon_updater_ops)
import bpy
from . import addon_updater_ops
from . import TempCtrls
from . import Rigger_Toolbox
from . import Tools
from . import Display
from . import emp
from . import ui
from . import multikey
from . import Rigger_Toolbox
from pathlib import Path
from bpy.utils import register_class
from bpy.utils import unregister_class
from bpy.app.handlers import persistent
import os
class TempCtrlsItems(bpy.types.PropertyGroup):
#located at context.scene.btc.ctrl_items
controlled: bpy.props.PointerProperty(name = "controlled object", description = "rigs and objects that are being controlled", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
controller: bpy.props.PointerProperty(name = "controller object", description = "rigs and objects that are controling", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
class TempCtrlsSceneSettings(bpy.types.PropertyGroup):
#located at context.scene.btc
root: bpy.props.BoolProperty(name = "Root Empty", description = "Add a root to the empties ", default = False, override = {'LIBRARY_OVERRIDABLE'}, update = TempCtrls.root_prop)
root_bone: bpy.props.StringProperty(name = "Root bone", description = "Root empty as a root bone ", override = {'LIBRARY_OVERRIDABLE'}, update = TempCtrls.root_update)
root_object: bpy.props.PointerProperty(name = "Root object", description = "Root empty as a root object ", update = TempCtrls.root_update, type = bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
ctrl_type: bpy.props.EnumProperty(name = 'Controllers', description="Select empties or a bone with a new rig to bake to", items = [('BONE', 'Bone','Bake to bones','BONE_DATA', 0), ('EMPTY', 'Empty', 'Bake to empties', 'EMPTY_ARROWS', 1)])
ctrl_items: bpy.props.CollectionProperty(type = TempCtrlsItems, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
bake_range_type: bpy.props.EnumProperty(name = 'Bake Range', description="Use either scene, actions length or custom frame range", default = 'KEYFRAMES', update= TempCtrls.update_range_type,
items = [('SCENE', 'Scene Range', 'Bake to the scene range'), ('KEYFRAMES', 'Keyframes Range', 'Bake all the keyframes in the layers'), ('CUSTOM', 'Custom', 'Enter a custom frame range')], override = {'LIBRARY_OVERRIDABLE'})
bake_range: bpy.props.IntVectorProperty(name='Frame Range', description='Bake to a custom frame range', size = 2, update= TempCtrls.update_bake_range)
bake_layers: bpy.props.BoolProperty(name = "Bake Layers", description = "Use keyframes from all the layers to include in the bake", default = False)
target: bpy.props.EnumProperty(name = 'Affect', description="Cleanup created constraints and empties", default = 1,
items = [('ALL', 'All Ctrl Rigs','Bake to all Ctrl Rigs', 0),
('SELECTED', 'Selected Chains','Bake to only selected chain controlls', 1),
('RELATIVE', 'Relative Ctrls Rig','Bake to the Relative Control rigs', 2)])
# ('CONSTRAINTS', 'Constraints', 'Clean all the bone constraints', 3),
# ('CONTROLLERS', 'Controllers', 'Remove all the baked empties', 4)])
selection: bpy.props.EnumProperty(name = 'Select', description="Select all controls, original bones or their relative", default = 'CONTROLLERS',
items = [('RELATIVE_CTRLS', 'Relative Ctrls','Select the Relative controller to your current selection', 0),
('RELATIVE_CONSTRAINED', 'Relative Constrained','Select the Relative original constrained bone to your current selection', 1),
('CONTROLLERS', 'All Ctrls', 'Select all the controller bones or empties', 2), ('CONSTRAINED', 'All Constrained', 'Select all the original constrained bones', 3)])
#smartbake setting
linksettings: bpy.props.BoolProperty(name = "Link Settings", description = "Link Settings", default = False, override = {'LIBRARY_OVERRIDABLE'})
bakesettings: bpy.props.BoolProperty(name = "bake settings", description = "bake settings", default = False, override = {'LIBRARY_OVERRIDABLE'})
cleansettings: bpy.props.BoolProperty(name = "clean settings", description = "clean settings", default = False, override = {'LIBRARY_OVERRIDABLE'})
smartbake: bpy.props.BoolProperty(name = "Smart Bake", description = "Keep Original Frame count", default = True, override = {'LIBRARY_OVERRIDABLE'})
inbetween_keyframes: bpy.props.IntProperty(name = "Inbetween Keyframes", description = "Add inbetween keyframes", default = 0, override = {'LIBRARY_OVERRIDABLE'})
from_origin: bpy.props.BoolProperty(name = "From Origin", description = "Use Keyframes from Original Bone", default = True, override = {'LIBRARY_OVERRIDABLE'})
from_ctrl: bpy.props.BoolProperty(name = "From Controller", description = "Use Keyframes from Controller Bone", default = False, override = {'LIBRARY_OVERRIDABLE'})
clean_ctrls: bpy.props.BoolProperty(name = "Remove Ctrls", description = "Remove Controls", default = True, override = {'LIBRARY_OVERRIDABLE'})
clean_constraints: bpy.props.BoolProperty(name = "Remove Constraints", description = "Remove Constraints", default = True, override = {'LIBRARY_OVERRIDABLE'})
rebake_to_org: bpy.props.BoolProperty(name = "ReBake connections to original bones", description = "ReBake ctrls from connected chains current anim to the original bones", default = False, override = {'LIBRARY_OVERRIDABLE'})
link_to: bpy.props.EnumProperty(name = 'Link to Chain', description="Link to begining of an active chain or the tip of the chain", default = 1,
items = [('BASE', 'Base','Link to the base of the active chain', 0), ('TIP', 'Tip', 'Link to the tip of the chain', 1)])
# link_from: bpy.props.EnumProperty(name = 'Link to Chain', description="Link to begining of an active chain or the tip of the chain", default = 0,
# items = [('BASE', 'Base','Link to the base of the active chain', 0), ('TIP', 'Tip', 'Link to the tip of the chain', 1)])
shape_size: bpy.props.FloatProperty(name='Size', description="Multiple factor for the shape size of the temp controls", update = TempCtrls.tempctrl_shapesize, min = 0.001, default = 1.5, override = {'LIBRARY_OVERRIDABLE'})#
shape_type: bpy.props.EnumProperty(name = 'Shape Type', description="Display type for the controls", items = TempCtrls.ctrl_shape_items, update = TempCtrls.tempctrl_shape_type)
color_set: bpy.props.EnumProperty(name="Bone Color Set", description="Choose a bone color set", items = TempCtrls.get_bone_color_sets, update = TempCtrls.update_bone_color, default = 9)
add_ik_ctrl: bpy.props.BoolProperty(name = 'Add an Extra IK Ctrl Bone', description = "Adds an extra bone ctrl as the ik ctrl", default = False, update = TempCtrls.add_ik_prop)
pole_target: bpy.props.BoolProperty(name = 'Add Pole Target', description = "Adding Pole Target to the IK Chain", default = True, update = TempCtrls.pole_prop)
pole_offset: bpy.props.FloatProperty(name="Offset", description="Offset the bone in the axis direction", default=1.0, update = TempCtrls.pole_offset)
child: bpy.props.BoolProperty(name = 'Add extra child Ctrls', description = "Add an child control for an overlay control", default = False, update = TempCtrls.child_prop)
orientation: bpy.props.BoolProperty(name = 'Use World Space Orientation', description = "Orient the bones to world space instead of to the original bones", default = True)
# enabled: bpy.props.BoolProperty(name = 'Switch On / Off', description = "Enabling and Disabling Temp Ctrls influence", default = True)
class TempCtrlsBoneSettings(bpy.types.PropertyGroup):
#located at obj.pose.bones[##].btc
root: bpy.props.BoolProperty(name = "Root Bone", description = "Bone is marked as the root bone", default = False, override = {'LIBRARY_OVERRIDABLE'})
child: bpy.props.BoolProperty(name = "Child Bone", description = "Bone is marked as a child bone inside the setup", default = False, override = {'LIBRARY_OVERRIDABLE'})
org_id: bpy.props.IntProperty(name = "Originate ID", description = "ID number of the bone the ctrl originates from", override = {'LIBRARY_OVERRIDABLE'})
setup_id: bpy.props.IntProperty(name = "Setup ID", description = "ID number of the current chain setup", override = {'LIBRARY_OVERRIDABLE'})
setup: bpy.props.EnumProperty(name = 'Setup Type', description="Describes what kind of setup the bone is part of", override = {'LIBRARY_OVERRIDABLE'},
items = [('NONE', 'No Setup','No Setup Applied', 0),
('WORLDSPACE', 'World Space Ctrl','World Space Ctrl setup', 1),
('TEMPFK', 'Temporary FK setup','Temporary FK chain setup', 2),
('TEMPFK_FLIP', 'Temporary flipped FK setup','Temporary flipped FK chain setup', 3),
('TEMPIK', 'Temporary IK setup','Temporary IK setup', 4),
('POLE', 'Temporary IK Pole setup','Temporary IK Pole', 5),
('PARENTCTRL', 'Parent Ctrl from cursor setup','Parent Ctrl from cursor setup', 6),
('ROOT', 'Root', 'Root Ctrl for all the setups', 7),
('EMPTY', 'Root', 'Root Ctrl for all the setups', 8),
('TRACK_TO', 'Track To','World Space Track to Ctrl setup', 9),
('TRACK_TO_EMPTY', 'Track To Empty','World Space Track to Empty Ctrl setup', 10)])
#using org mostly to decide if it needs a custom shape
org: bpy.props.EnumProperty(name = 'Org Type', description="Describes what if the function of the bone", override = {'LIBRARY_OVERRIDABLE'},
items = [('CTRL', 'Controller Bone','Controller Bone', 0),
('ORG', 'Original Bone','Original Bone', 1),
('MCH', 'Mechanical Bone','Mechanical Bone', 2),
('NONE', 'Nothing applied','Nothing applied', 3)])
shape: bpy.props.BoolProperty(name = "Apply shape", description = "Mark if the bone needs a shape", default = False, override = {'LIBRARY_OVERRIDABLE'})
class TempCtrlsObjectSetups(bpy.types.PropertyGroup):
#located at obj.animtoolbox.ctrl_setups
#name: using string of the id
setup: bpy.props.EnumProperty(name = 'Setup Type', description="Describes what kind of setup the bone is part of",
items = [('NONE', 'No Setup','No Setup Applied', 0),
('WORLDSPACE', 'World Space Ctrl','World Space Ctrl setup', 1),
('TEMPFK', 'Temporary FK setup','Temporary FK chain setup', 2),
('TEMPFK_FLIP', 'Temporary flipped FK setup','Temporary flipped FK chain setup', 3),
('TEMPIK', 'Temporary IK setup','Temporary IK setup', 4),
('PARENTCTRL', 'Parent Ctrl from cursor setup','Parent Ctrl from cursor setup', 5),
('ROOT', 'Root', 'Root Ctrl for all the setups', 6),
('EMPTY', 'Root', 'Root Ctrl for all the setups', 8),
('TRACK_TO', 'Track To','World Space Track to Ctrl setup', 9),
('TRACK_TO_EMPTY', 'Track To Empty','World Space Track to Empty Ctrl setup', 10),
])
class MultikeyProperties(bpy.types.PropertyGroup):
selectedbones: bpy.props.BoolProperty(name="Selected Bones", description="Affect only selected bones", default=True, options={'HIDDEN'})
handletype: bpy.props.BoolProperty(name="Keep handle types", description="Keep handle types", default=False, options={'HIDDEN'})
scale: bpy.props.FloatProperty(name="Scale Factor", description="Scale percentage of the average value", default=1.0, update = multikey.scale_value)
randomness: bpy.props.FloatProperty(name="Randomness", description="Random Threshold of keyframes", default=0.1, min=0.0, max = 1.0, update = multikey.random_value)
class AnimToolBoxObjectSettings(bpy.types.PropertyGroup):
controlled: bpy.props.PointerProperty(name = 'Controlled Rig', description="Adding the rig object that is being controlled by the current object", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
controller: bpy.props.PointerProperty(name = 'Controller Rig', description="Adding the rig object that is used as the temp control object", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
ctrl_setups: bpy.props.CollectionProperty(type = TempCtrlsObjectSetups, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
ctrls_enabled: bpy.props.BoolProperty(name = 'Temp Ctrls Switch', description = "Enabling and Disabling Temp Ctrls influence", default = True)
# influence: bpy.props.FloatProperty(name = "Influence Slider for the Temp Ctrls", description = "Switching the influence slider for the temp ctrls", default = 1, min = 0.0, max = 1.0)
#Used for Bake to Empties
# org_id: bpy.props.IntProperty(name = "Originate ID", description = "ID number of the bone the ctrl originates from", override = {'LIBRARY_OVERRIDABLE'})
setup_id: bpy.props.IntProperty(name = "Setup ID", description = "ID number of the current chain setup", override = {'LIBRARY_OVERRIDABLE'})
root: bpy.props.BoolProperty(name = "Root Empty", description = "Empty is marked as the root ctrl", default = False, override = {'LIBRARY_OVERRIDABLE'})
child: bpy.props.BoolProperty(name = "Child Empty", description = "Empty is marked as a child bone inside the setup", default = False, override = {'LIBRARY_OVERRIDABLE'})
keyframes_offset: bpy.props.FloatProperty(name = "Keyframes Offset", description = "Interactive slider to offset keyframes back and forth ", default = 0)
class IsolatedRigs(bpy.types.PropertyGroup):
hidden: bpy.props.PointerProperty(name = "Hidden Rigs", description = "List of Rigs that are hidden during pose mode isolation", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
selected: bpy.props.PointerProperty(name = "Selected Rigs", description = "List of Rigs that are hidden during pose mode isolation", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
class AnimToolBoxUILayout(bpy.types.PropertyGroup):
'''Layout properties for the UI'''
quick_menu: bpy.props.BoolProperty(name = "Use Quick Menu", description = "Opens header menu with only icon", default = False)
copy_paste_matrix: bpy.props.BoolProperty(name = "Copy Matrix Menu", description = "Opens the menu for copy paste matrices", default = True)
copy_paste_world: bpy.props.BoolProperty(name = "Copy Paste World Matrix", description = "Copy and Paste the World Matrix", default = False, update = Tools.copy_paste_world_update)
copy_paste_relative: bpy.props.BoolProperty(name = "Copy Paste Relative Matrix", description = "Copy and Paste the Matrix relative to the active bone", default = False, update = Tools.copy_paste_relative_update)
Inbetweens: bpy.props.BoolProperty(name = "Blendings/Inbetweens", description = "Opens the menu for Inbetweens", default = True)
gizmo_size: bpy.props.BoolProperty(name = "Gizmo size", description = "Change the Gizmo size using alt +/- hotkeys", default = False)
# temp_ctrls: bpy.props.BoolProperty(name = "Temp Ctrls", description = "Open Temp Ctrls", default = False)
temp_ctrls_switch: bpy.props.BoolProperty(name = "Temp Ctrls Switch", description = "Temp Ctrls Switch", default = True)
temp_ctrls_shapes: bpy.props.BoolProperty(name = "Temp Ctrls Shapes", description = "Temp Ctrls Shapes", default = True)
markers_retimer: bpy.props.BoolProperty(name = "Marker Retimer", description = "Flag when marker retimer turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
relative_cursor: bpy.props.BoolProperty(name = "Relative Cursor Mode", description = "Cursor moves relative to the selection", default = False)
is_dragging: bpy.props.BoolProperty(default = False)
#using Blending sliders in the window manager to avoid undo issues with modal operators
inbetween_worldmatrix: bpy.props.FloatProperty(name='Inbetween World Matrix', description="Adds an inbetween of the World Matrix to the Layer's neighbor keyframes", soft_min = -1, soft_max = 1, default=0.0, update = Tools.add_inbetween_worldmatrix, override = {'LIBRARY_OVERRIDABLE'})
blend_mirror: bpy.props.FloatProperty(name='Blend Mirror', description="Blend into the mirrored pose", soft_min = 0, soft_max = 1, default=0, step = 1, update = Tools.blend_to_mirror, override = {'LIBRARY_OVERRIDABLE'})
multikey: bpy.props.PointerProperty(type = MultikeyProperties, options={'LIBRARY_EDITABLE'}, override = {'LIBRARY_OVERRIDABLE'})
class AnimToolBoxGlobalSettings(bpy.types.PropertyGroup):
#context.scene.animtoolbox
marker_frame_range: bpy.props.BoolProperty(name = "Marker Frame Range", description = "Flag when marker frame range turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
bake_frame_range: bpy.props.BoolProperty(name = "Bake Frame Range", description = "Flag when marker bake range turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
# markers_retimer: bpy.props.BoolProperty(name = "Marker Retimer", description = "Flag when marker retimer turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
keyframes_offset: bpy.props.FloatProperty(name = "Keyframes Offset", description = "Interactive slider to offset keyframes back and forth ", soft_max = 2, soft_min = -2, default = 0, update = Tools.keyframes_offset_slider)
rotation_mode: bpy.props.EnumProperty(name = 'Rotation Mode', description="Describes what kind of setup the bone is part of", override = {'LIBRARY_OVERRIDABLE'},
items = [('QUATERNION', 'Quaternion','Quaternion Rotation Order - No Gimbal Lock', 0),
('XYZ', 'XYZ', 'XYZ Rotation Order', 1), ('XZY', 'XZY','XZY Rotation Order', 2),
('YXZ', 'YXZ','YXZ Rotation Order', 3), ('YZX', 'YZX', 'YZX Rotation Order', 4),
('ZXY', 'ZXY', 'ZXY Rotation Order', 5), ('ZYX', 'ZYX', 'ZYX Rotation Order', 6),
('AXIS_ANGLE', 'AXIS_ANGLE', 'Axis Angle Rotation Order', 7)])
isolate_pose_mode: bpy.props.BoolProperty(name = "Isolate rig in pose mode", description = "Isolates the rig during pose mode, rigs in object mode are hidden", default = False)
active_obj: bpy.props.PointerProperty(name = "Active Object", description = "Current Active Object", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
isolated: bpy.props.CollectionProperty(type = IsolatedRigs, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
#Blendings
inbetweener : bpy.props.FloatProperty(name='Inbetween Keyframe', description="Adds an inbetween Keyframe between the Layer's neighbor keyframes", soft_min = -1, soft_max = 1, default=0.0, options = set(), update = Tools.add_inbetween_key, override = {'LIBRARY_OVERRIDABLE'})
motion_path: bpy.props.BoolProperty(name = "Motion Path", description = "Flag when Motion Path is on", default = False, override = {'LIBRARY_OVERRIDABLE'})
mp_settings: bpy.props.BoolProperty(name = "Motion Path Settings", description = "Open the settings Menu", default = False, override = {'LIBRARY_OVERRIDABLE'})
mp_keyframe_scale: bpy.props.FloatProperty(name = "Scale Selecgted Keyframes Bounding Box", description = "Change the scale of the bounding box around the selected keyframes ", default = 0.1, step = 0.1, precision = 3)
mp_color_before: bpy.props.FloatVectorProperty(name="Motion Path Before Color", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, description="Motion path color before the current frame")
mp_color_after: bpy.props.FloatVectorProperty(name="Motion Path After Color", subtype='COLOR', default=(0.0, 1.0, 0.0), min=0.0, max=1.0, description="Motion path color before the current frame")
mp_infront: bpy.props.BoolProperty(name = "Motion Path In Front", description = "Display motion path in front of all the objects", default = True, override = {'LIBRARY_OVERRIDABLE'})
mp_points: bpy.props.BoolProperty(name = "Motion Path Points", description = "Display motion path points", default = True, override = {'LIBRARY_OVERRIDABLE'})
mp_lines: bpy.props.BoolProperty(name = "Motion Path Lines", description = "Display motion path lines", default = True, override = {'LIBRARY_OVERRIDABLE'})
mp_handles: bpy.props.BoolProperty(name = "Motion Path Handles", description = "Display motion path handles on keyframe selection", default = True, override = {'LIBRARY_OVERRIDABLE'})
mp_display_frames: bpy.props.BoolProperty(name = "Frame Numbers", description = "Display frame numbers on all the keyframes", default = False, override = {'LIBRARY_OVERRIDABLE'})
mp_handle_types: bpy.props.EnumProperty(name = 'Set Keyframe Handle Type', description="Set handle type for selected keyframes", default = 'AUTO', update = emp.update_handle_type_prop,
items = [('FREE', 'Free', 'Free', 'HANDLE_FREE', 0),
('ALIGNED','Aligned', 'Aligned', 'HANDLE_ALIGNED', 1),
('VECTOR', 'Vector', 'Vector', 'HANDLE_VECTOR', 2),
('AUTO','Automatic', 'Automatic', 'HANDLE_AUTO', 3),
('AUTO_CLAMPED','Auto Clamped', 'Auto Clamped', 'HANDLE_AUTOCLAMPED', 4)])
mp_interpolation: bpy.props.EnumProperty(name = 'Set Interpolation', description="Set Keyframe Interpolation", default = 'BEZIER', update = emp.update_interpolation_prop,
items = [('BEZIER', 'Bezier', 'Bezier', 'IPO_BEZIER', 0),
('LINEAR','Linear', 'Linear', 'IPO_LINEAR', 1),
('CONSTANT', 'Constant', 'Constant', 'IPO_CONSTANT', 2)])
mp_frame_range: bpy.props.EnumProperty(name = 'Frame Range', description="Type of Frame Range", default = 'SCENE', #update = emp.mp_frame_range_update,
items = [('KEYS_ALL', 'All_Keys','Use the Scene Frame Length for the Range', 0),
('SCENE', 'Scene','Use the Scene Frame Length for the Range', 1),
('MANUAL', 'Manual','Custom Frame range using numerical input or the markers frame ranger', 2),
('AROUND', 'Around Frames','Show only around the current frame', 3)])
mp_before: bpy.props.IntProperty(name = "Before", description = "Show the frames Before the current frame", min = 0, default = 10)
mp_after: bpy.props.IntProperty(name = "After", description = "Show the frames After the current frame", min = 0, default = 10)
selected_keyframes: bpy.props.StringProperty(name="Selected Keyframes", description="Serialized representation of selected keyframes")
gizmo_size: bpy.props.IntProperty(name = "Add to Gizmo Size", description = "Addition to Gizmo Size", max = 100, min = -100, default = 10)
#Copy/Pase Matrix
range_type: bpy.props.EnumProperty(name = 'Paste to Frames', description="Paste to current frame or a range of frames.", update = Tools.bake_range_type,
items = [('CURRENT', 'Current Frame','Paste Matrix to only current frame', 0),
('SELECTED', 'Selected Keyframe','Paste Matrix to only selected keyframes', 1),
('RANGE', 'Frame Range','Paste Matrix to a Frame Range', 2)])
bake_frame_start: bpy.props.IntProperty(name = "Bake Frame Start", description = "Define the start frame to paste the matrix", min = 0, update = Tools.bake_frame_start_limit)
bake_frame_end: bpy.props.IntProperty(name = "Bake Frame End", description = "Define the end frame to paste the matrix", min = 0, update = Tools.bake_frame_end_limit)
filter_location: bpy.props.BoolVectorProperty(name="Location", description="Filter Location properties", default=(False, False, False), size = 3, options={'HIDDEN'}, update = Tools.filter_name_update)
filter_rotation: bpy.props.BoolVectorProperty(name="Rotation", description="Filter Rotation properties", default=(False, False, False, False), size = 4, options={'HIDDEN'}, update = Tools.filter_name_update)
filter_scale: bpy.props.BoolVectorProperty(name="Scale", description="Filter Scale properties", default=(False, False, False), size = 3, options={'HIDDEN'}, update = Tools.filter_name_update)
#The name displayed on the filter button
filter_name: bpy.props.StringProperty(name="Filter Name", description="Change the name of the button while chaging the filter options", default= "", options={'HIDDEN'})
filter_custom_props: bpy.props.BoolProperty(name = "Filter Custom Properties", description = "Filter custom properties", default = False)
filter_keyframes: bpy.props.BoolProperty(name = "Filter Aelected Keyframes", description = "Filter selected keyframes for specific tools", default = False)
col_vis: bpy.props.BoolProperty(name = "Animated collections visibility", description = "Display if animated collections are turned on or off", default = False)
@addon_updater_ops.make_annotations
class AnimToolBoxPreferences(bpy.types.AddonPreferences):
# this must match the addon name, use '__package__'
# when defining this in a submodule of a python package.
bl_idname = __package__
category: bpy.props.StringProperty(
name="Tab Category",
description="Choose a name for the category of the panel",
default="Animation",
update=ui.update_panel
)
quick_menu: bpy.props.BoolProperty(name = "Use Quick Menu", description = "Opens header menu with only icon", default = False)
riggertoolbox: bpy.props.BoolProperty(name = "RiggerToolBox", description = "Include RiggerToolbox (experimental)", default = False, update = ui.add_riggertoolbox)
multikey: bpy.props.BoolProperty(name = "Multikey", description = "Include Multikey for adju\sting multiply keyframes", default = False, update = ui.add_multikey)
#Temp Ctrls properties
in_front: bpy.props.BoolProperty(name = "Always In Front", description = "Set Temp Ctrls to be always in front", default = True)
clear_setup : bpy.props.BoolProperty(name = "Clear Selection Before Creating New Temp Ctrls", description = "Clear old setup when adding Temp ctrls to an existing chain", default = False)
#Editable motion path
keyframes_range: bpy.props.IntProperty(name = "Keyframe Range", description = "The range of distance from the keyframes while hovering over them", min = 5, max = 100, default = 15)
# addon updater preferences from `__init__`, be sure to copy all of them
auto_check_update: bpy.props.BoolProperty(
name = "Auto-check for Update",
description = "If enabled, auto-check for updates using an interval",
default = True,
)
updater_interval_months: bpy.props.IntProperty(
name='Months',
description = "Number of months between checking for updates",
default=0,
min=0
)
updater_interval_days: bpy.props.IntProperty(
name='Days',
description = "Number of days between checking for updates",
default=7,
min=0,
)
updater_interval_hours: bpy.props.IntProperty(
name='Hours',
description = "Number of hours between checking for updates",
default=0,
min=0,
max=23
)
updater_interval_minutes: bpy.props.IntProperty(
name='Minutes',
description = "Number of minutes between checking for updates",
default=0,
min=0,
max=59
)
#Draw the UI in the preferences
def draw(self, context):
layout = self.layout
addon_updater_ops.update_settings_ui(self, context)
row = layout.row()
col = row.column()
col.label(text="Tab Category:")
col.prop(self, "category", text="")
layout.separator()
col = layout.column()
col.prop(self, "quick_menu", text="Use Quick Icons Menu")
layout.separator()
box = layout.box()
row = box.row()
row.label(text = 'Temp Ctrls: ')
row = box.row()
row.prop(self, 'clear_setup')
row.prop(self, 'in_front')
layout.separator()
col = layout.column()
col.label(text = 'Include Extras: ')
row = layout.row()
row.prop(self, "multikey", text="Multikey - Edit Multiply keyframes")
row.prop(self, "riggertoolbox", text="RiggerToolBox (Experimental)")
@persistent
def loadanimtoolbox_pre(self, context):
scene = bpy.context.scene
dns = bpy.app.driver_namespace
if scene.animtoolbox.bake_frame_range:
scene.animtoolbox.bake_frame_range = False
if scene.animtoolbox.motion_path:
scene.animtoolbox.motion_path = False
if 'mp_dh' in dns:
bpy.types.SpaceView3D.draw_handler_remove(dns['mp_dh'], 'WINDOW')
bpy.app.driver_namespace.pop('mp_dh')
if 'markers_retimer_dh' in dns:
bpy.types.SpaceView3D.draw_handler_remove(dns['markers_retimer_dh'], 'WINDOW')
bpy.app.driver_namespace.pop('markers_retimer_dh')
#remove the motion path app handler if it's still inside
if emp.mp_value_update in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(emp.mp_value_update)
if emp.mp_frame_change in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.remove(emp.mp_frame_change)
if emp.mp_undo_update in bpy.app.handlers.undo_pre:
bpy.app.handlers.undo_pre.remove(emp.mp_undo_update)
@persistent
def loadanimtoolbox_post(self, context):
scene = bpy.context.scene
dns = bpy.app.driver_namespace
if scene.animtoolbox.isolate_pose_mode:
if Display.isolate_pose_mode not in bpy.app.handlers.depsgraph_update_pre:
bpy.app.handlers.depsgraph_update_pre.append(Display.isolate_pose_mode)
if scene.animtoolbox.motion_path:
scene.animtoolbox.motion_path = False
if 'mp_dh' in dns:
bpy.types.SpaceView3D.draw_handler_remove(dns['mp_dh'], 'WINDOW')
bpy.app.driver_namespace.pop('mp_dh')
Tools.selection_order(self, context)
classes = (TempCtrlsItems, TempCtrlsObjectSetups, TempCtrlsSceneSettings,TempCtrlsBoneSettings, MultikeyProperties, IsolatedRigs,
AnimToolBoxObjectSettings, AnimToolBoxUILayout, AnimToolBoxGlobalSettings) + ui.classes
addon_keymaps = []
def register():
# Note that preview collections returned by bpy.utils.previews
# are regular py objects - you can use them to store custom data.
addon_updater_ops.register(bl_info)
register_class(AnimToolBoxPreferences)
addon_updater_ops.make_annotations(AnimToolBoxPreferences) # to avoid blender 2.8 warnings
TempCtrls.register()
Tools.register()
Display.register()
emp.register()
ui.register_custom_icon()
for cls in classes:
# print(cls)
register_class(cls)
bpy.types.Scene.btc = bpy.props.PointerProperty(type = TempCtrlsSceneSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.PoseBone.btc = bpy.props.PointerProperty(type = TempCtrlsBoneSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.Object.animtoolbox = bpy.props.PointerProperty(type = AnimToolBoxObjectSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.Scene.animtoolbox = bpy.props.PointerProperty(type = AnimToolBoxGlobalSettings, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.WindowManager.atb_ui = bpy.props.PointerProperty(type = AnimToolBoxUILayout, override = {'LIBRARY_OVERRIDABLE'})
ui.update_panel(None, bpy.context)
ui.add_multikey(None, bpy.context)
ui.add_riggertoolbox(None, bpy.context)
if loadanimtoolbox_pre not in bpy.app.handlers.load_pre:
bpy.app.handlers.load_pre.append(loadanimtoolbox_pre)
if loadanimtoolbox_post not in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.append(loadanimtoolbox_post)
if Tools.selection_order not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(Tools.selection_order)
#Make sure TAB hotkey in the NLA goes into full stack mode
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
km = kc.keymaps.new(name= '3D View', space_type= 'VIEW_3D')
if 'view3d.gizmo_size_up' not in km.keymap_items:
kmi = km.keymap_items.new('view3d.gizmo_size_up', type= 'NUMPAD_PLUS', value= 'PRESS', alt = True, repeat = True)
addon_keymaps.append((km, kmi))
if 'view3d.gizmo_size_down' not in km.keymap_items:
kmi = km.keymap_items.new('view3d.gizmo_size_down', type= 'NUMPAD_MINUS', value= 'PRESS', alt = True, repeat = True)
addon_keymaps.append((km, kmi))
#Add Tools to the Toolbar
bpy.utils.register_tool(ui.KeyframeOffsetTool, separator=True)
#Add tools to the menu
bpy.types.VIEW3D_MT_editor_menus.append(ui.draw_menu)
def unregister():
for pcoll in ui.preview_collections.values():
bpy.utils.previews.remove(pcoll)
ui.preview_collections.clear()
#addon_updater_ops.unregister()
addon_updater_ops.unregister()
unregister_class(AnimToolBoxPreferences)
TempCtrls.unregister()
# Rigger_Toolbox.unregister()
Tools.unregister()
Display.unregister()
emp.unregister()
ui.add_multikey(None, bpy.context)
ui.add_riggertoolbox(None, bpy.context)
for cls in classes:
unregister_class(cls)
bpy.utils.unregister_tool(ui.KeyframeOffsetTool)
#Remove the header menu ui
bpy.types.VIEW3D_MT_editor_menus.remove(ui.draw_menu)
del bpy.types.Scene.btc
# del bpy.types.Bone.btc
del bpy.types.Object.animtoolbox
del bpy.types.Scene.animtoolbox
if hasattr(bpy.types.Object, 'keyframes_offset'):
del bpy.types.Object.keyframes_offset
if hasattr(bpy.types.PoseBone, 'keyframes_offset'):
del bpy.types.PoseBone.keyframes_offset
if loadanimtoolbox_pre in bpy.app.handlers.load_pre:
bpy.app.handlers.load_pre.remove(loadanimtoolbox_pre)
if loadanimtoolbox_post in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(loadanimtoolbox_post)
if Tools.selection_order in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(Tools.selection_order)
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
if __name__ == "__main__":
register()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
{
"last_check": "2025-06-19 15:30:39.178174",
"backup_date": "",
"update_ready": true,
"ignore": false,
"just_restored": false,
"just_updated": false,
"version_text": {
"link": "https://gitlab.com/api/v4/projects/45739913/repository/archive.zip?sha=198933935077e89aa87550163d3747388f2552e8",
"version": [
0,
0,
7,
3
]
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,527 @@
# ***** 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
import random
import numpy as np
from mathutils import Quaternion
from . import Tools
def attr_default(obj, fcu_key):
#check if the fcurve source belongs to a bone or obj
if fcu_key[0][:10] == 'pose.bones':
transform = fcu_key[0].split('.')[-1]
attr = fcu_key[0].split('"')[-2]
bone = fcu_key[0].split('"')[1]
if bone in obj.pose.bones:
source = obj.pose.bones[bone]
#if the bone not found still calculate the default based on the path
elif '.rotation_quaternion' in fcu_key[0]:
return [1.0, 0.0, 0.0, 0.0]
elif '.scale' in fcu_key[0]:
return [1.0, 1.0, 1.0]
else:
return [0]
#in case of shapekey animation
elif fcu_key[0][:10] == 'key_blocks':
attr = fcu_key[0].split('"')[1]
if attr not in obj.data.shape_keys.key_blocks:
return [0]
shapekey = obj.data.shape_keys.key_blocks[attr]
return 0 if shapekey.slider_min <= 0 else shapekey.slider_min
#in case of transforms in object mode
else:# fcu_key[0] in transform_types:
source = obj
transform = fcu_key[0]
#check when it's transform property of Blender
if transform in source.bl_rna.properties.keys():
if hasattr(source.bl_rna.properties[transform], 'default_array'):
if len(source.bl_rna.properties[transform].default_array) > fcu_key[1]:
attrvalue = source.bl_rna.properties[transform].default_array
return attrvalue
#in case of property on object
elif len(fcu_key[0].split('"')) > 1:
if fcu_key[0].split('"')[1] in obj.keys():
attr = fcu_key[0].split('"')[1]
if 'attr' not in locals():
return [0]
#since blender 3 access to custom property settings changed
if attr in source:
if not isinstance(source[attr], float) and not isinstance(source[attr], int):
return [0]
id_attr = source.id_properties_ui(attr).as_dict()
attrvalue = id_attr['default']
return [attrvalue]
return [0]
def store_handles(key):
#storing the distance between the handles bezier to the key value
handle_r = key.handle_right[1] - key.co[1]
handle_l = key.handle_left[1] - key.co[1]
return handle_r, handle_l
def apply_handles(key, handle_r, handle_l):
key.handle_right[1] = key.co[1] + handle_r
key.handle_left[1] = key.co[1] + handle_l
def selected_bones_filter(obj, fcu_data_path):
if not bpy.context.window_manager.atb_ui.multikey.selectedbones:
#if not obj.als.onlyselected:
return False
if obj.mode != 'POSE':
return True
transform_types = ['location', 'rotation_euler', 'rotation_quaternion', 'scale']
#filter selected bones if option is turned on
bones = [bone.path_from_id() for bone in bpy.context.selected_pose_bones]
if fcu_data_path.split('].')[0]+']' not in bones and fcu_data_path not in transform_types:
return True
# def filter_properties(obj, fcu):
# 'Filter the W X Y Z attributes of the transform properties'
# transform = fcu.data_path.split('"].')[1] if obj.mode == 'POSE' else fcu.data_path
# index = fcu.array_index
# if 'rotation' in transform:
# if transform == 'rotation_euler':
# index -= 1
# transform = 'rotation'
# transform = 'filter_' + transform
# if not hasattr(bpy.context.scene.multikey, transform):
# return False
# attr = getattr(bpy.context.scene.multikey, transform)
# #print(fcu.data_path, index, transform, attr[index])
# return True if attr[index] else False
def add_value(key, value):
if key.select_control_point:
#store handle values in relative to the keyframe value
handle_r, handle_l = store_handles(key)
key.co[1] += value
apply_handles(key, handle_r, handle_l)
#calculate the difference between current value and the fcurve value
def add_diff(fcurves, path, current_value, eval_array):
array_value = current_value - eval_array
if not any(array_value):
return
for i, value in enumerate(array_value):
fcu = fcurves.find(path, index = i)
if fcu is None or Tools.filter_properties(bpy.context.scene.animtoolbox, fcu):
continue
for key in fcu.keyframe_points:
add_value(key, value)
fcu.update()
class ScaleValuesOp(bpy.types.Operator):
"""Modal operator used while scale value is running before release"""
bl_idname = "animtoolbox.multikey_scale_value"
bl_label = "Scale Values"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
#reset the values for dragging
self.stop = False
ui = context.window_manager.atb_ui
ui['is_dragging'] = True
self.avg_value = dict()
#dictionary of the keyframes and their original INITIAL values
self.keyframes_values = dict()
self.keyframes_handle_right = dict()
self.keyframes_handle_left = dict()
#the average value for each fcurve
self.keyframes_avg_value = dict()
for obj in context.selected_objects:
if obj.animation_data.action is None:
continue
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
for fcu in fcurves:
if obj.mode == 'POSE':
if selected_bones_filter(obj, fcu.data_path):
continue
if Tools.filter_properties(context.scene.animtoolbox, fcu):
continue
#avg and value list per fcurve
avg_value = []
value_list = []
for key in fcu.keyframe_points:
if key.select_control_point:
value_list.append(key.co[1])
self.keyframes_values.update({key : key.co[1]})
self.keyframes_handle_right.update({key : key.handle_right[1]})
self.keyframes_handle_left.update({key : key.handle_left[1]})
if len(value_list)>1:
#the average value with the scale property added to it
avg_value = sum(value_list) / len(value_list)
for key in fcu.keyframe_points:
if key.select_control_point:
self.keyframes_avg_value.update({key : avg_value})
if not self.keyframes_avg_value:
ui['is_dragging'] = False
ui.multikey['scale'] = 1
Tools.redraw_areas(['VIEW_3D'])
return {'CANCELLED'}
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
ui = context.window_manager.atb_ui
scale = ui.multikey.scale
#Quit the modal operator when the slider is released
if self.stop:
ui['is_dragging'] = False
ui.multikey['scale'] = 1
Tools.redraw_areas(['VIEW_3D'])
#modal is being cancelled because of undo issue with the modal running through the property
return {'FINISHED'}
if event.value == 'RELEASE': # Stop the modal on next frame. Don't block the event since we want to exit the field dragging
self.stop = True
return {'PASS_THROUGH'}
for key, key_value in self.keyframes_values.items():
if not key.select_control_point:
continue
if key not in self.keyframes_avg_value:
continue
avg_value = self.keyframes_avg_value[key]
handle_right_value = self.keyframes_handle_right[key]
handle_left_value = self.keyframes_handle_left[key]
#add the value of the distance from the average * scale factor
key.co[1] = avg_value + ((key_value - avg_value)*scale)
key.handle_right[1] = avg_value + ((handle_right_value - avg_value)*scale)
key.handle_left[1] = avg_value + ((handle_left_value - avg_value)*scale)
return {'PASS_THROUGH'}
def scale_value(self, context):
ui = context.window_manager.atb_ui
if ui.is_dragging:
return
obj = context.object
if obj is None:
self['scale'] = 1
return
action = obj.animation_data.action
if action is None:
self['scale'] = 1
return
if context.mode == 'POSE' and not context.selected_pose_bones:
self['scale'] = 1
return
bpy.ops.animtoolbox.multikey_scale_value('INVOKE_DEFAULT')
def random_value(self, context):
for obj in context.selected_objects:
if obj.animation_data.action is None:
continue
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
for fcu in fcurves:
if obj.mode == 'POSE':
if selected_bones_filter(obj, fcu.data_path):
continue
if Tools.filter_properties(context.scene.animtoolbox, fcu):
continue
value_list = []
threshold = bpy.context.window_manager.atb_ui.multikey.randomness
for key in fcu.keyframe_points:
if key.select_control_point == True:
value_list.append(key.co[1])
if len(value_list) > 0:
value = max(value_list)- min(value_list)
for key in fcu.keyframe_points:
add_value(key, value * random.uniform(-threshold, threshold))
fcu.update()
def evaluate_combine(data_path, added_array, eval_array, array_default, influence):
if 'scale' in data_path:
eval_array = eval_array * (added_array / array_default) ** influence
elif 'rotation_quaternion' in data_path:
#multiply first the influence with the w separatly
added_array[0] = added_array[0] + (1- added_array[0])*(1 - influence)
added_array[1:] *= influence
eval_array = np.array(Quaternion(eval_array) @ Quaternion(added_array))# ** influence
#if it's a custom property
elif 'rotation_euler' not in data_path and 'location' not in data_path:
eval_array = eval_array + (added_array - array_default) * influence
return eval_array
def evaluate_array(fcurves, fcu_path, frame, array_default = [0, 0, 0]):
'''Create an array from all the indexes'''
array_len = len(array_default)
fcu_array = []
#get the missing arrays in case quaternion is not complete
missing_arrays = []
for i in range(array_len):
fcu = fcurves.find(fcu_path, index = i)
if fcu is None:
missing_arrays.append(i)
continue
fcu_array.append(fcu.evaluate(frame))
#In case it's a quaternion and missing attributes, then adding from default value
if fcu_array and array_len == 4 and missing_arrays:
for i in missing_arrays:
fcu_array.insert(i, array_default[i])
if not len(fcu_array):
return None
return np.array(fcu_array)
def evaluate_layers(context, obj, anim_data, fcu, array_default):
'''Calculate the evaluation of all the layers when using the nla'''
if not hasattr(anim_data, 'nla_tracks') or not anim_data.use_nla:
return None
nla_tracks = anim_data.nla_tracks
if not len(nla_tracks):
return None
frame = context.scene.frame_current
blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
fcu_path = fcu.data_path
eval_array = array_default
for track in nla_tracks:
if track.mute:
continue
if not len(track.strips):
continue
for strip in track.strips:
if not strip.frame_start < frame < strip.frame_end:
continue
action = strip.action
if action is None:
continue
blend_type = strip.blend_type
#get the influence value either from the attribute or the fcurve. function coming from bake
influence = strip.influence
if len(strip.fcurves):
if not strip.fcurves[0].mute and len(strip.fcurves[0].keyframe_points):
influence = strip.fcurves[0].evaluate(frame)
#evaluate the frame according to the strip settings
frame_eval = frame
#change the frame if the strip is on hold
if frame < strip.frame_start:
if strip.extrapolation == 'HOLD':
frame_eval = strip.frame_start
elif frame >= strip.frame_end:
if strip.extrapolation == 'HOLD' or strip.extrapolation == 'HOLD_FORWARD':
frame_eval = strip.frame_end
last_frame = strip.frame_start + (strip.frame_end - strip.frame_start) / strip.repeat
if strip.repeat > 1 and (frame) >= last_frame:
action_range = (strip.action_frame_end * strip.scale - strip.action_frame_start * strip.scale)
frame_eval = (((frame_eval - strip.frame_start) % (action_range)) + strip.frame_start)
if strip.use_reverse:
frame_eval = last_frame - (frame_eval - strip.frame_start)
offset = (strip.frame_start * 1/strip.scale - strip.action_frame_start) * strip.scale
frame_eval = strip.frame_start * 1/strip.scale + (frame_eval - strip.frame_start) * 1/strip.scale - offset * 1/strip.scale
fcurves = Tools.get_fcurves_channelbag(obj, action)
eval_array = evaluate_blend_type(fcurves, eval_array, fcu_path, frame_eval, influence, array_default, blend_type, blend_types)
#Adding an extra layer from the action outside and on top of the nla
tweak_mode = anim_data.use_tweak_mode
if tweak_mode:
anim_data.use_tweak_mode = False
action = anim_data.action
if action:
influence = anim_data.action_influence
blend_type = anim_data.action_blend_type
fcurves = Tools.get_fcurves_channelbag(obj, action)
eval_array = evaluate_blend_type(fcurves, eval_array, fcu_path, frame, influence, array_default, blend_type, blend_types)
anim_data.use_tweak_mode = tweak_mode
return eval_array
def evaluate_blend_type(fcurves, eval_array, fcu_path, frame, influence,
array_default, blend_type, blend_types):
'''Calculate the value based on the blend type'''
fcu_array = evaluate_array(fcurves, fcu_path, frame, array_default)
if fcu_array is None:
return eval_array
###EVALUATION###
if blend_type =='COMBINE':
if 'location' in fcu_path or 'rotation_euler' in fcu_path:
blend_type = 'ADD'
if blend_type =='REPLACE':
eval_array = eval_array * (1 - influence) + fcu_array * influence
elif blend_type =='COMBINE':
eval_array = evaluate_combine(fcu_path, fcu_array, eval_array, array_default, influence)
else:
eval_array = eval('eval_array' + blend_types[blend_type] + 'fcu_array' + '*' + str(influence))
return eval_array
def evaluate_value(self, context):
ui = context.window_manager.atb_ui
for obj in context.selected_objects:
anim_data = obj.animation_data
if anim_data is None:
return
if anim_data.action is None:
return
action = obj.animation_data.action
fcu_paths = []
transformations = ["rotation_quaternion","rotation_euler", "location", "scale"]
if obj.mode == 'POSE':
bonelist = context.selected_pose_bones if ui.multikey.selectedbones else obj.pose.bones
fcurves = Tools.get_fcurves_channelbag(obj, action)
for fcu in fcurves:
if fcu in fcu_paths:
continue
if Tools.filter_properties(context.scene.animtoolbox, fcu):
continue
if obj.mode == 'POSE':
if selected_bones_filter(obj, fcu.data_path):
continue
for bone in bonelist:
#find the fcurve of the bone
if fcu.data_path.rfind(bone.name) != 12 or fcu.data_path[12 + len(bone.name)] != '"':
continue
path_split = fcu.data_path.split('"].')
if len(path_split) <= 1:
continue
else:
transform = fcu.data_path.split('"].')[1]
if transform not in transformations:
continue
current_value = getattr(obj.pose.bones[bone.name], transform)
else:
transform = fcu.data_path
current_value = getattr(obj, transform)
array_default = np.array(attr_default(obj, (fcu.data_path, fcu.array_index)))
# array_default = np.array([attr_default(obj, (fcu.data_path, i)) for i in range(4)
# if fcurves.find(fcu.data_path, index = i) is not None])
eval_array = evaluate_layers(context, obj, anim_data, fcu, array_default)
if eval_array is None:
fcurves = Tools.get_fcurves_channelbag(obj, action)
eval_array = evaluate_array(fcurves, fcu.data_path, context.scene.frame_current, array_default)
#calculate the difference between current value and the fcurve value
add_diff(fcurves, fcu.data_path, np.array(current_value), eval_array)
class MULTIKEY_OT_Multikey(bpy.types.Operator):
"""Edit all selected keyframes"""
bl_label = "Edit Selected Keyframes"
bl_idname = "animtoolbox.multikey"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.animation_data and bpy.context.scene.tool_settings.use_keyframe_insert_auto == False
def execute(self, context):
evaluate_value(self, context)
return {'FINISHED'}
class MultikeyProperties(bpy.types.PropertyGroup):
selectedbones: bpy.props.BoolProperty(name="Selected Bones", description="Affect only selected bones", default=True, options={'HIDDEN'})
handletype: bpy.props.BoolProperty(name="Keep handle types", description="Keep handle types", default=False, options={'HIDDEN'})
scale: bpy.props.FloatProperty(name="Scale Factor", description="Scale percentage of the average value", default=1.0, update = scale_value)
randomness: bpy.props.FloatProperty(name="Randomness", description="Random Threshold of keyframes", default=0.1, min=0.0, max = 1.0, update = random_value)
#filters
# filter_location: bpy.props.BoolVectorProperty(name="Location", description="Filter Location properties", default=(True, True, True), size = 3, options={'HIDDEN'})
# filter_rotation: bpy.props.BoolVectorProperty(name="Rotation", description="Filter Rotation properties", default=(True, True, True, True), size = 4, options={'HIDDEN'})
# filter_scale: bpy.props.BoolVectorProperty(name="Scale", description="Filter Scale properties", default=(True, True, True), size = 3, options={'HIDDEN'})
# class FilterProperties(bpy.types.Operator):
# """Filter Location Rotation and Scale Properties"""
# bl_idname = "fcurves.filter"
# bl_label = "Filter Properties W X Y Z"
# bl_options = {'REGISTER', 'UNDO'}
# def invoke(self, context, event):
# 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 = 'Location')
# row.prop(context.scene.multikey, 'filter_location', text = '')
# row = layout.row()
# row.label(text = 'Rotation')
# row.prop(context.scene.multikey, 'filter_rotation', text = '')
# row = layout.row()
# row.label(text = 'Scale')
# row.prop(context.scene.multikey, 'filter_scale', text = '')
# def execute(self, context):
# return {'CANCELLED'}
classes = (ScaleValuesOp, MULTIKEY_OT_Multikey)
#register, unregister = bpy.utils.register_classes_factory(classes)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
# bpy.types.Scene.animtoolbox.multikey = bpy.props.PointerProperty(type = MultikeyProperties, options={'LIBRARY_EDITABLE'}, override = {'LIBRARY_OVERRIDABLE'})
def unregister():
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)
# del bpy.types.Scene.animtoolbox.multikey
@@ -0,0 +1,700 @@
# ***** 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 . import Rigger_Toolbox
from . import multikey
from . import Rigger_Toolbox
from pathlib import Path
from bpy.utils import register_class
from bpy.utils import unregister_class
import os
######################################################## MENUS ########################################################
class ANIMTOOLBOX_MT_Copy_Paste_Matrix(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Copy_Paste_Matrix'
bl_label = "Copy Paste Matrix"
@classmethod
def poll(cls, context):
return True
def draw(self, context):
layout = self.layout
layout.operator('anim.copy_matrix', text = 'Copy World Matrix', icon ='DUPLICATE')
layout.operator('anim.paste_matrix', text = 'Paste World Matrix', icon ='PASTEDOWN')
layout.separator()
layout.operator('anim.copy_relative_matrix', text = 'Copy Relative Matrix', icon ='DUPLICATE')
layout.operator('anim.paste_relative_matrix', text = 'Paste Relative Matrix', icon ='PASTEDOWN')
layout.separator()
layout.prop_menu_enum(context.scene.animtoolbox, 'range_type', text = 'Frame Range Type')
if context.scene.animtoolbox.range_type == 'RANGE':
# split = layout.split(factor = 0.3)
# layout.prop(context.scene.animtoolbox, 'bake_frame_start', text = 'Start')
# layout.prop(context.scene.animtoolbox, 'bake_frame_end', text = 'End')
layout.operator("anim.markers_bakerange", icon = 'MARKER', text ='Frame Range Markers Widget', depress = context.scene.animtoolbox.bake_frame_range)
class ANIMTOOLBOX_MT_Temp_Ctrls(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Temp_Ctrls'
bl_label = "Temp Ctrls"
@classmethod
def poll(cls, context):
return True
def draw(self, context):
layout = self.layout
custom_icons = preview_collections["main"]
layout.operator('anim.bake_to_ctrl', text="World Space Ctrls", icon = 'WORLD')
layout.operator('anim.bake_to_temp_fk', text="Temp FK Ctrls", icon = 'BONE_DATA')
layout.operator('anim.bake_to_temp_ik', text="Temp IK Ctrls", icon = 'CON_KINEMATIC')
layout.separator()
layout.operator('anim.link_temp_chains', text="Link Temp Chains", icon = 'DECORATE_LINKED')
layout.operator('anim.unlink_temp_chains', text="UnLink Temp Chains", icon = 'DECORATE_LINKED')
layout.separator()
layout.operator('anim.bake_temp_ctrls', text="Bake Temp Ctrls", icon_value = custom_icons["oven"].icon_id)
layout.operator('anim.remove_bones_constraints', text="Cleanup", icon_value = custom_icons["trash"].icon_id)
layout.prop_menu_enum(context.scene.btc, 'target', text = 'Bake To', icon = 'MOD_ARMATURE')
layout.separator()
ctrl = context.object.animtoolbox.controller if context.object.animtoolbox.controller else context.object
if ctrl:
layout.prop(ctrl.animtoolbox, 'ctrls_enabled', text ='Temp Ctrls On/Off')
# layout.operator('anim.enable_tempctrls', text="On" , icon = 'HIDE_OFF')#icon = 'CONSTRAINT_BONE'
# layout.operator('anim.disable_tempctrls', text="Off", icon = 'HIDE_ON')
layout.separator()
layout.prop_menu_enum(context.scene.btc, 'shape_type', icon = 'MESH_DATA')
layout.prop(context.scene.btc, 'shape_size', slider = False, icon ='CON_SIZELIKE')
layout.prop(context.scene.btc, 'color_set', text = '')
# layout.prop(context.scene.btc, 'shape_type', icon = 'MESH_DATA', text ='')
class ANIMTOOLBOX_MT_Keyframe_Offset(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Keyframe_Offset'
bl_label = "Interactive Keyframe Offset"
def draw(self, context):
layout = self.layout
layout.prop(context.scene.animtoolbox, 'keyframes_offset', slider = False)
layout.operator('anim.select_keyframes_offset', text="Select Offset Keyframes", icon ='RESTRICT_SELECT_OFF')
layout.operator('anim.apply_keyframes_offset', text="Apply Offset", icon = 'ANIM')
class ANIMTOOLBOX_MT_Blendings(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Blendings'
bl_label = "Blendings"
def draw(self, context):
layout = self.layout
ui = context.window_manager.atb_ui
layout.prop(ui, 'inbetween_worldmatrix', slider = True)
layout.prop(context.scene.animtoolbox, 'inbetweener', slider = True)
layout.prop(ui, 'blend_mirror', slider = True)
class ANIMTOOLBOX_MT_Multikey(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Multikey'
bl_label = "Multikey"
def draw(self, context):
layout = self.layout
ui = context.window_manager.atb_ui
layout.operator("animtoolbox.multikey", icon = 'ACTION_TWEAK')
layout.prop(ui.multikey, 'scale', slider = True)
layout.prop(ui.multikey, 'randomness', slider = True)
class ANIMTOOLBOX_MT_Convert_Rotations(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Convert_Rotations'
bl_label = "Convert Rotations"
def draw(self, context):
layout = self.layout
layout.operator("anim.convert_rotation_mode", icon = 'DRIVER_ROTATIONAL_DIFFERENCE', text = 'Convert Rotation To')
layout.operator("anim.find_rotation_mode", icon = 'VIEWZOOM', text = 'Recommend Euler Rotation')
layout.prop_menu_enum(context.scene.animtoolbox, 'rotation_mode', text = 'Convert To')
class ANIMTOOLBOX_MT_operators(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_menu_operators'
bl_label = "AnimToolBox"
@classmethod
def poll(cls, context):
return True
def draw(self, context):
layout = self.layout
scene = context.scene
ui = context.window_manager.atb_ui
custom_icons = preview_collections["main"]
if context.area.type == 'VIEW_3D':
layout.menu('ANIMTOOLBOX_MT_Temp_Ctrls', text = 'Temp Ctrls', icon_value = custom_icons["puppet"].icon_id)
layout.separator()
layout.operator('anim.share_keyframes', text = 'Share Keyframes', icon_value = custom_icons["sharekeys"].icon_id)
layout.operator('anim.relative_cursor', text = 'Relative Cursor Pivot', icon_value = custom_icons["relative_cursor"].icon_id, depress = ui.relative_cursor)
layout.operator("anim.markers_retimer", icon_value = custom_icons["retime"].icon_id, text ='Markers Retimer', depress = ui.markers_retimer)
layout.menu('ANIMTOOLBOX_MT_Convert_Rotations', text = 'Convert Rotations', icon = 'DRIVER_ROTATIONAL_DIFFERENCE')
layout.separator()
layout.menu('ANIMTOOLBOX_MT_Keyframe_Offset', text = 'Interactive Keyframe Offset', icon_value = custom_icons["keyframe_offset"].icon_id)
layout.menu('ANIMTOOLBOX_MT_Blendings', text = 'Blendings and inbetweens', icon_value = custom_icons["sliders"].icon_id)
layout.separator()
layout.menu('ANIMTOOLBOX_MT_Copy_Paste_Matrix', text = 'Copy Paste Matrix', icon_value = custom_icons["copy_matrix"].icon_id)
if bpy.context.preferences.addons[__package__].preferences.multikey:
layout.separator()
layout.menu('ANIMTOOLBOX_MT_Multikey', text = 'Multikey', icon_value = custom_icons["multikey"].icon_id)
layout.separator(factor = 0.5)
layout.operator('anim.switch_collections_visibility', icon = 'COLLECTION_COLOR_06', text = 'Animated Collections Visibilty')
layout.operator('anim.isolate_pose_mode', icon_value = custom_icons["isolate"].icon_id, depress = scene.animtoolbox.isolate_pose_mode)
layout.operator('object.motion_path_operator', text = 'Editable Motion Path', depress = scene.animtoolbox.motion_path, icon_value = custom_icons["mt"].icon_id)
layout.separator()
layout.prop(context.preferences.addons[__package__].preferences, 'quick_menu', text = 'Use Quick Icons Menu')
# layout.prop(context.window_manager.atb_ui, 'quick_menu', text = 'Use Quick Icons Menu')
# elif context.area.type == 'DOPESHEET_EDITOR':
# layout.operator('wm.call_menu_pie', text="Pie AnimOffset").name = 'ANIMAIDE_MT_pie_anim_offset'
# layout.menu('ANIMAIDE_MT_anim_offset')
# layout.menu('ANIMAIDE_MT_anim_offset_mask')
# elif context.area.type == 'GRAPH_EDITOR':
# layout.menu('ANIMAIDE_MT_curve_tools_pie')
######################################################## WorkSpaceTools ########################################################
class KeyframeOffsetTool(bpy.types.WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'POSE'
bl_idname = 'animtoolbox.keyframe_offset'
bl_label = 'Keyframe Offset Tool'
bl_description = (
'Offset the keyframes of the selected bone\n'
'Shift + Click to select the bones with an offset\n'
'Ctrl + Alt + Click to Apply the offset'
)
bl_icon = (Path(__file__).parent / "icons" / "ops.anim.keyframe_offset").as_posix()
bl_keymap = (
('anim.keyframe_offset', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG'}, None),
# ('anim.select_keyframes_offset', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'shift': True}, None),
('anim.apply_keyframes_offset', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'ctrl': True, 'alt': True}, None),
('view3d.select_box', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG', 'shift': True}, {'properties': [('mode', 'ADD')]}),
('view3d.select_box', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG', 'ctrl': True}, {'properties': [('mode', 'SUB')]}),
('view3d.select_box', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG', 'shift': True, 'ctrl': True}, {'properties': [('mode', 'AND')]}),
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK'}, {'properties': [('deselect_all', True)]}),
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'shift': True}, {'properties': [('toggle', True)]}),
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'alt': True}, {'properties': [('enumerate', True)]}),
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'shift': True, 'alt': True}, {'properties': [('toggle', True), ('enumerate', True)]})
)
######################################################## PANELS ########################################################
class ANIMTOOLBOX_PT_Panel:
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Animation"
#bl_options = {"DEFAULT_CLOSED"}
@classmethod
def poll(cls, context):
return context.object is not None
class ANIMTOOLBOX_PT_MainPanel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
#bl_category = "Animation"
bl_label = "AnimToolBox"
bl_idname = "ANIMTOOLBOX_PT_MainPanel"
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
custom_icons = preview_collections["main"]
self.layout.label(text='', icon_value = custom_icons["animtoolbox"].icon_id)
def draw(self, context):
obj = context.object
if obj is None:
return
# layout = self.layout
# row = layout.row(align = True)
# row.label(text = 'Support me on ')
# row.operator("wm.url_open", text="Patreon", icon = 'FUND').url = "https://www.patreon.com/animtoolbox"
class TEMPCTRLS_PT_Panel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
#bl_category = "Animation"
bl_label = "Temp Controls"
bl_idname = "TEMPCTRLS_PT_Panel"
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
custom_icons = preview_collections["main"]
self.layout.label(text="", icon_value = custom_icons["puppet"].icon_id)
def draw(self, context):
obj = context.object
btc = context.scene.btc
if obj is None:
return
custom_icons = preview_collections["main"]
oven_icon = custom_icons["oven"]
# row = layout.row()
ui = context.window_manager.atb_ui
layout = self.layout
box = layout.box()
col = box.column()
col.operator('anim.bake_to_ctrl', text="WorldSpace Ctrls", icon ='WORLD') #
col.operator('anim.bake_to_temp_fk', text="Temp FK Ctrls", icon = 'BONE_DATA')
col.operator('anim.bake_to_temp_ik', text="Temp IK Ctrls", icon = 'CON_KINEMATIC')
# layout.separator(factor = 0.1)
# box = layout.box()
row = box.row()
row.operator('anim.link_temp_chains', text="Link Temp Chains", icon = 'DECORATE_LINKED')
row.prop(btc, 'linksettings', text = '', icon = 'SETTINGS')
if btc.linksettings:
# insidebox = box.box()
row = box.row()
row.label(text = 'Link to Active Chain: ')
row.prop(btc, 'link_to', text = '')
row = box.row()
row.operator('anim.unlink_temp_chains', text="UnLink Temp Chains", icon = 'UNLINKED')
# row = insidebox.row()
# row.label(text = 'Link from Chain: ')
# row.prop(btc, 'link_from', text = '')
# layout.separator(factor = 0.1)
box = layout.box()
row = box.row()
row.operator('anim.bake_to_empties', text="WorldSpace Empties")
row.operator('anim.empties_to_bones', text="", icon = 'EMPTY_AXIS')
box = layout.box()
row = box.row()
# row.operator('anim.bake_constrained_bones', text="Quick Bake", icon = 'REC')
row.operator('anim.bake_temp_ctrls', text="Bake Temp Ctrls", icon_value = oven_icon.icon_id)
row.prop(btc, 'bakesettings', text = '', icon = 'SETTINGS')
if btc.bakesettings:
insidebox = box.box()
# split = insidebox.split(factor = 0.9)
# split.prop(btc, 'smartbake', text = 'Smart Bake')
# split.operator('fcurves.filter_ui', icon ='FILTER', text = '')
# if btc.smartbake:
# row.prop(btc, 'inbetween_keyframes')
split = insidebox.split(factor = 0.3)
split.label(text = 'Bake To:')
# split.split(factor = 0.9)
split_2 = split.split(factor = 0.9)
split_2.prop(btc, 'target', text = '')
split_2.operator('fcurves.filter_ui', icon ='FILTER', text = '')
split = insidebox.split(factor = 0.3)
split.label(text = 'Bake Range:')
split.prop(btc, 'bake_range_type', text = '')
if btc.bake_range_type == 'KEYFRAMES':
row = insidebox.row()
row.label(text = 'Keyframes: ')
row.prop(btc, 'smartbake', text = 'Smart Bake')
row.prop(btc, 'bake_layers', text = 'All Layers')
row = insidebox.row()
row.label(text = 'From:')
row.prop(btc, 'from_origin', text = 'Origin')
row.prop(btc, 'from_ctrl', text = 'Ctrls')
elif btc.bake_range_type == 'CUSTOM':
row = insidebox.row()
row.prop(btc, 'bake_range', text = '')
insidebox.separator(factor=0.1)
row = insidebox.row()
row.label(text = 'Clean: ')
row.prop(btc, 'clean_constraints', text = 'Constraints')
if btc.clean_constraints:
row.prop(btc, 'clean_ctrls', text = 'Ctrls')
row = box.row()
row.operator('anim.remove_bones_constraints', text="Cleanup", icon_value = custom_icons["trash"].icon_id)
row.prop(btc, 'cleansettings', text = '', icon = 'SETTINGS')
if btc.cleansettings:
insidebox = box.box()
split = insidebox.split(factor = 0.4)
split.label(text = 'Target: ')
split.prop(btc, 'target', text = '')
row = insidebox.row()
row.label(text = 'Clean: ')
row.prop(btc, 'clean_constraints', text = 'Constraints')
if btc.clean_constraints:
row.prop(btc, 'clean_ctrls', text = 'Ctrls')
if btc.target == 'SELECTED':
split = insidebox.split(factor = 0.68)
split.label(text = 'ReBake Link Ctrls to Original: ')
split.prop(btc, 'rebake_to_org', text = '')
box = layout.box()
row = box.row()
# row = layout.row()
# row.prop(obj.animtoolbox, 'influence', text = 'Influence', slider = True)
ctrl = obj.animtoolbox.controller if obj.animtoolbox.controller else obj
if ctrl:
enabled_icon = 'HIDE_OFF' if ctrl.animtoolbox.ctrls_enabled else 'HIDE_ON'
row.prop(ctrl.animtoolbox, 'ctrls_enabled', text ='Temp Ctrls On/Off', icon = enabled_icon)
# row = box.row()
# row.prop(ui, 'temp_ctrls_switch', icon = 'RESTRICT_VIEW_OFF', text = '')
# row.label(text = 'Temp Ctrls Switch: ')
# row.prop(btc, 'target', text = '', icon = 'MOD_ARMATURE', icon_only = True)
# if ui.temp_ctrls_switch:
# # split = layout.split(factor = 0.225)
# row = box.row()
# row.operator('anim.enable_tempctrls', text="On" , icon = 'HIDE_OFF')#icon = 'CONSTRAINT_BONE'
# row.operator('anim.disable_tempctrls', text="Off", icon = 'HIDE_ON')
box.separator(factor=0.1)
row = box.row()
row.prop(ui, 'temp_ctrls_shapes', icon = 'MESH_DATA', text = '')
row.label(text = 'Temp Ctrls Shapes: ')
row.prop(btc, 'target', text = '', icon = 'MOD_ARMATURE', icon_only = True)
if ui.temp_ctrls_shapes:
row = box.row()
row.prop(btc, 'shape_size', slider = False)
row.prop(btc, 'shape_type', text = '')
row = layout.row()
# split.label(text = 'Theme Color: ')
row.prop(btc, 'color_set', text = '')
layout.separator()
row = layout.row()
row.operator('anim.btc_selections', text="Select", icon = 'CONSTRAINT_BONE')
row.prop(btc, 'selection', text = '', icon = 'CON_ARMATURE')
class ANIMTOOLBOX_PT_Tools(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
bl_label = "Anim Tools"
bl_idname = "ANIMTOOLBOX_PT_Tools"
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
custom_icons = preview_collections["main"]
layout = self.layout
layout.label(text="", icon_value = custom_icons["toolbox"].icon_id)
def draw(self, context):
obj = context.object
scene = context.scene
atb = scene.animtoolbox
if obj is None:
return
custom_icons = preview_collections["main"]
layout = self.layout
ui = context.window_manager.atb_ui
row = layout.row()
filter_text = 'Filter Tools Properties' if atb.filter_name == '' else 'Filters: ' + atb.filter_name
filter_depress = False if atb.filter_name == '' else True
row.operator('fcurves.filter_ui', icon ='FILTER', text = filter_text, depress = filter_depress)
layout.separator()
#layout.operator('anim.keyframes_offset', text="Offset Keyframes", icon = 'NEXT_KEYFRAME')
split = layout.split(factor = 0.9)
split.prop(context.scene.animtoolbox, 'keyframes_offset', slider = True)
split.operator('anim.apply_keyframes_offset', text="", icon_value = custom_icons["apply"].icon_id)
split.operator('anim.select_keyframes_offset', text="", icon_value = custom_icons["select"].icon_id)
row = layout.row()
row.operator('anim.share_keyframes', text = 'Share Keyframes', icon_value = custom_icons["sharekeys"].icon_id)
row = layout.row()
row.operator("anim.relative_cursor", text ='Relative Cursor Pivot', icon_value = custom_icons["relative_cursor"].icon_id, depress = ui.relative_cursor)
row = layout.row()
row.operator("anim.markers_retimer", icon_value = custom_icons["retime"].icon_id, text ='Markers Retimer', depress = ui.markers_retimer)
layout.separator(factor = 0.5)
box = layout.box()
# icon = 'DOWNARROW_HLT' if ui.copy_paste_matrix is True else 'RIGHTARROW_THIN'
# split = box.split(factor = 0.9)
# split.prop(ui, 'copy_paste_matrix', icon_value = custom_icons["copy_matrix"].icon_id, text = 'Copy Paste Matrices')
# split.operator('fcurves.filter', icon ='FILTER', text = '')
row = box.row()
row.prop(ui, 'copy_paste_matrix', icon_value = custom_icons["copy_matrix"].icon_id, text = 'Copy Paste Matrices')
if ui.copy_paste_matrix:
row = box.row()
row.label(text = 'Copy World Matrix :', icon = 'WORLD')
row.operator('anim.copy_matrix', text = '', icon ='DUPLICATE')
row.operator('anim.paste_matrix', text = '', icon ='PASTEDOWN')
row = box.row()
row.label(text = 'Copy Relative Matrix :', icon = 'LINKED')
row.operator('anim.copy_relative_matrix', text = '', icon ='DUPLICATE')
row.operator('anim.paste_relative_matrix', text = '', icon ='PASTEDOWN')
split = box.split(factor = 0.32)
split.label(text = 'Paste To')
split.prop(context.scene.animtoolbox, 'range_type', text = '')
if context.scene.animtoolbox.range_type == 'RANGE':
row = box.row()
row.prop(context.scene.animtoolbox, 'bake_frame_start', text = 'Start')
row.prop(context.scene.animtoolbox, 'bake_frame_end', text = 'End')
row.operator("anim.markers_bakerange", icon = 'MARKER', text ='', depress = context.scene.animtoolbox.bake_frame_range)
layout.separator(factor = 0.5)
box = layout.box()
# icon = 'DOWNARROW_HLT' if ui.Inbetweens is True else 'RIGHTARROW_THIN'
# split = box.split(factor = 0.9)
# split.prop(ui, 'Inbetweens', icon_value = custom_icons["sliders"].icon_id, text = 'Blendings / Inbetweens')
# split.operator('fcurves.filter', icon ='FILTER', text = '')
row = box.row()
row.prop(ui, 'Inbetweens', icon_value = custom_icons["sliders"].icon_id, text = 'Blendings / Inbetweens')
if ui.Inbetweens:
col = box.column()
col.prop(ui, 'inbetween_worldmatrix', slider = True)
col.prop(scene.animtoolbox, 'inbetweener', slider = True)
col.prop(ui, 'blend_mirror', slider = True)
layout.separator(factor = 1)
# my_icon = custom_icons["rotations"]
split = layout.split(factor = 0.6)
split.operator("anim.convert_rotation_mode", icon = 'DRIVER_ROTATIONAL_DIFFERENCE', text = 'Convert Rotation To')
# split.operator("anim.convert_rotation_mode", text = 'Convert Rotation To', icon_value = my_icon.icon_id)
split = split.split(factor = 0.3)
split.operator("anim.find_rotation_mode", icon = 'VIEWZOOM', text = '')
split.prop(context.scene.animtoolbox, 'rotation_mode', text = '')
class MULTIKEY_PT_Panel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
"""Add random value to selected keyframes"""
bl_label = "Multikey"
bl_idname = "MULTIKEY_PT_Panel"
# bl_space_type = 'VIEW_3D'
# bl_region_type = 'UI'
# bl_category= 'Animation'
bl_parent_id = 'ANIMTOOLBOX_PT_Tools'
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
custom_icons = preview_collections["main"]
layout = self.layout
layout.label(text="", icon_value = custom_icons["multikey"].icon_id)
def draw(self, context):
custom_icons = preview_collections["main"]
layout = self.layout
ui = context.window_manager.atb_ui
# layout.separator()
split = layout.split(factor=0.1, align = True)
split.prop(ui.multikey, 'selectedbones', icon_value = custom_icons["selected_bones"].icon_id, text = '')
# split = split.split(factor=0.1, align = True)
#split.operator('fcurves.filter', icon ='FILTER', text = '')
split.operator("animtoolbox.multikey", icon = 'ACTION_TWEAK')
layout.separator()
row = layout.row(align = True)
row.prop(ui.multikey, 'scale')
row.prop(ui.multikey, 'randomness', slider = True)
class ANIMTOOLBOX_PT_Display(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
bl_label = "Display"
bl_idname = "ANIMTOOLBOX_PT_Display"
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
custom_icons = preview_collections["main"]
self.layout.label(text="", icon_value = custom_icons["display"].icon_id)
def draw(self, context):
scene = context.scene
layout = self.layout
ui = context.window_manager.atb_ui
custom_icons = preview_collections["main"]
box = layout.box()
row = box.row()
icon = 'DOWNARROW_HLT' if ui.gizmo_size is True else 'RIGHTARROW_THIN'
row.prop(ui, 'gizmo_size', icon = icon, text = 'Add to Gizmo Size:')
if ui.gizmo_size:
row = box.row()
row.prop(scene.animtoolbox, 'gizmo_size', text = '')
row.operator('view3d.gizmo_size_up', text="", icon ='ADD')
row.operator('view3d.gizmo_size_down', text="", icon ='REMOVE')
layout.separator(factor = 0.5)
row = layout.row()
col_vis_icon = 'OUTLINER_COLLECTION' if scene.animtoolbox.col_vis else 'COLLECTION_COLOR_06'
row.operator('anim.switch_collections_visibility', icon = col_vis_icon, text = 'Animated Collections Visibilty', depress = scene.animtoolbox.col_vis)
layout.separator(factor = 0.5)
row = layout.row()
row.operator('anim.isolate_pose_mode', icon_value = custom_icons["isolate"].icon_id, depress = scene.animtoolbox.isolate_pose_mode)
layout.separator(factor = 0.5)
box = layout.box()
row = box.row()
row.operator('object.motion_path_operator', text = 'Editable Motion Path', depress = scene.animtoolbox.motion_path, icon_value = custom_icons["mt"].icon_id)
row.prop(scene.animtoolbox, 'mp_settings', text = '', icon = 'SETTINGS')
if scene.animtoolbox.mp_settings:
row = box.row()
row.prop(scene.animtoolbox, 'mp_color_before', text = '')
row.prop(scene.animtoolbox, 'mp_color_after', text = '')
row = box.row()
row.prop(scene.animtoolbox, 'mp_keyframe_scale', text = 'Scale Keyframes Box', icon = 'CUBE')
row = box.row()
row.prop(scene.animtoolbox, 'mp_points', text = 'Points')
row.prop(scene.animtoolbox, 'mp_lines', text = 'Lines')
row = box.row()
row.prop(scene.animtoolbox, 'mp_handles', text = 'Handles')
row.prop(scene.animtoolbox, 'mp_infront', text = 'In Front')
row = box.row()
row.prop(scene.animtoolbox, 'mp_display_frames', text = 'Display Keyframe Numbers')
row = box.row()
row.prop(context.preferences.addons[__package__].preferences, 'keyframes_range', text = 'Keyframe Distance Range')
if context.object is None:
return
split = box.split(factor = 0.4)
split.label(text ='Frame Range')
# row.prop(scene.animtoolbox, 'mp_frame_range', text = '')
split.prop(context.scene.animtoolbox, 'mp_frame_range', text = '')
if context.scene.animtoolbox.mp_frame_range == 'MANUAL':
row = box.row()
row.prop(context.scene.animtoolbox, 'bake_frame_start', text = 'Start')
row.prop(context.scene.animtoolbox, 'bake_frame_end', text = 'End')
row.operator("anim.markers_bakerange", icon = 'MARKER', text ='', depress = scene.animtoolbox.bake_frame_range)
elif context.scene.animtoolbox.mp_frame_range == 'AROUND':
row = box.row()
row.prop(context.scene.animtoolbox, 'mp_before', text = 'Before')
row.prop(context.scene.animtoolbox, 'mp_after', text = 'After')
# row.operator("anim.markers_bakerange", icon = 'MARKER', text ='', depress = scene.animtoolbox.bake_frame_range)
class RIGGERTOOLBOX_PT_Panel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
bl_label = "Rigger Toolbox"
bl_idname = "RIGGERTOOLBOX_PT_Panel"
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context):
obj = context.object
if obj is None:
return
layout = self.layout
col = layout.column()
col.operator("armature.add_bbone_widgets", text = "Add Bbone Widgets", icon = 'IPO_BEZIER')
col.separator()
col.operator("armature.add_chain_ctrls", text = "Add Chain Controls", icon = 'OUTLINER_DATA_ARMATURE')
col.separator()
col.operator("armature.merge", text = "Merge Rigs", icon = 'MOD_ARMATURE')
# Add-ons Preferences Update Panel
# Define Panel classes for updating
panels = [ANIMTOOLBOX_PT_MainPanel, TEMPCTRLS_PT_Panel, ANIMTOOLBOX_PT_Tools, ANIMTOOLBOX_PT_Display]
def update_panel(self, context):
message = "Animtoolbox: Updating Panel locations has failed"
try:
for panel in panels:
if "bl_rna" in panel.__dict__:
unregister_class(panel)
for panel in panels:
panel.bl_category = context.preferences.addons[__package__].preferences.category
register_class(panel)
except Exception as e:
print("\n[{}]\n{}\n\nError:\n{}".format(__package__, message, e))
pass
def add_multikey(self, context):
if bpy.context.preferences.addons[__package__].preferences is None:
return
if bpy.context.preferences.addons[__package__].preferences.multikey:
multikey.register()
register_class(MULTIKEY_PT_Panel)
register_class(ANIMTOOLBOX_MT_Multikey)
panels.append(MULTIKEY_PT_Panel)
#panels = panels + (MULTIKEY_PT_Panel)
elif hasattr(bpy.types, 'MULTIKEY_PT_Panel'):
multikey.unregister()
panels.remove(MULTIKEY_PT_Panel)
unregister_class(MULTIKEY_PT_Panel)
unregister_class(ANIMTOOLBOX_MT_Multikey)
def add_riggertoolbox(self, context):
if bpy.context.preferences.addons[__package__].preferences is None:
return
if bpy.context.preferences.addons[__package__].preferences.riggertoolbox:
Rigger_Toolbox.register()
register_class(RIGGERTOOLBOX_PT_Panel)
panels.append(RIGGERTOOLBOX_PT_Panel)
#panels = panels + (RIGGERTOOLBOX_PT_Panel)
elif hasattr(bpy.types, 'RIGGERTOOLBOX_PT_Panel'):
Rigger_Toolbox.unregister()
panels.remove(RIGGERTOOLBOX_PT_Panel)
unregister_class(RIGGERTOOLBOX_PT_Panel)
classes = (ANIMTOOLBOX_MT_Copy_Paste_Matrix, ANIMTOOLBOX_MT_Temp_Ctrls, ANIMTOOLBOX_MT_operators, ANIMTOOLBOX_MT_Keyframe_Offset,
ANIMTOOLBOX_MT_Blendings, ANIMTOOLBOX_MT_Convert_Rotations) + tuple(panels)
preview_collections = {}
#list of all the custom icons
icons_list = {'relative_cursor' : 'relative_cursor.png', 'puppet' : 'puppet.png', 'animtoolbox' : 'animtoolbox.png',
'sliders' : 'slider-navigation.png', 'world_space' : 'earth-rotation.png', 'isolate' : 'isolation.png',
'mt' : 'curve.png', 'toolbox' : 'tools.png', 'oven' : 'oven.png', 'retime' : 'retime.png', 'copy_matrix' : 'copy_world.png',
'sharekeys' : 'sharekeys.png', 'sliders' : 'sliders.png', 'keyframe_offset' : 'keyframes_offset.png', 'display' : 'display.png',
'switch' : 'switch.png', 'trash' : 'trash.png', 'apply' : 'apply.png', 'select' : 'select.png', 'worldspace' : 'WorldSpace.png',
'multikey' : 'multikey.png', 'selected_bones' : 'selected_bones.png'}
def register_custom_icon():
# Note that preview collections returned by bpy.utils.previews
# are regular py objects - you can use them to store custom data.
import bpy.utils.previews
custom_icons = bpy.utils.previews.new()
# path to the folder where the icon is
# the path is calculated relative to this py file inside the addon folder
icons_dir = os.path.join(os.path.dirname(__file__), "icons")
# load a preview thumbnail of a file and store in the previews collection
for iconname, icon_file in icons_list.items():
custom_icons.load(iconname, os.path.join(icons_dir, icon_file), 'IMAGE')
preview_collections["main"] = custom_icons
def draw_menu(self, context):
if context.mode == 'OBJECT' or context.mode == 'POSE':
layout = self.layout
scene = context.scene
ui = context.window_manager.atb_ui
custom_icons = preview_collections["main"]
layout.menu('ANIMTOOLBOX_MT_menu_operators') #, icon_value = custom_icons["toolbox"].icon_id, text =''
if not context.preferences.addons[__package__].preferences.quick_menu:
return
#Only Icons Menu
temp_ctrls = custom_icons["puppet"]
layout.menu('ANIMTOOLBOX_MT_Temp_Ctrls' , icon_value = temp_ctrls.icon_id, text ='')
# layout.separator()
layout.menu('ANIMTOOLBOX_MT_Copy_Paste_Matrix' , icon_value = custom_icons["copy_matrix"].icon_id, text ='')
layout.separator()
layout.operator('anim.relative_cursor', text = '', depress = ui.relative_cursor, icon_value = custom_icons["relative_cursor"].icon_id)
layout.separator()
layout.operator('anim.markers_retimer', text = '', depress = ui.markers_retimer, icon_value = custom_icons["retime"].icon_id)
layout.separator()
layout.operator('anim.isolate_pose_mode', text = '', depress = scene.animtoolbox.isolate_pose_mode, icon_value = custom_icons["isolate"].icon_id)
layout.separator()
layout.operator('object.motion_path_operator', text = '', depress = scene.animtoolbox.motion_path, icon_value = custom_icons["mt"].icon_id)
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+527
View File
@@ -0,0 +1,527 @@
# ***** 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
import random
import numpy as np
from mathutils import Quaternion
from . import Tools
def attr_default(obj, fcu_key):
#check if the fcurve source belongs to a bone or obj
if fcu_key[0][:10] == 'pose.bones':
transform = fcu_key[0].split('.')[-1]
attr = fcu_key[0].split('"')[-2]
bone = fcu_key[0].split('"')[1]
if bone in obj.pose.bones:
source = obj.pose.bones[bone]
#if the bone not found still calculate the default based on the path
elif '.rotation_quaternion' in fcu_key[0]:
return [1.0, 0.0, 0.0, 0.0]
elif '.scale' in fcu_key[0]:
return [1.0, 1.0, 1.0]
else:
return [0]
#in case of shapekey animation
elif fcu_key[0][:10] == 'key_blocks':
attr = fcu_key[0].split('"')[1]
if attr not in obj.data.shape_keys.key_blocks:
return [0]
shapekey = obj.data.shape_keys.key_blocks[attr]
return 0 if shapekey.slider_min <= 0 else shapekey.slider_min
#in case of transforms in object mode
else:# fcu_key[0] in transform_types:
source = obj
transform = fcu_key[0]
#check when it's transform property of Blender
if transform in source.bl_rna.properties.keys():
if hasattr(source.bl_rna.properties[transform], 'default_array'):
if len(source.bl_rna.properties[transform].default_array) > fcu_key[1]:
attrvalue = source.bl_rna.properties[transform].default_array
return attrvalue
#in case of property on object
elif len(fcu_key[0].split('"')) > 1:
if fcu_key[0].split('"')[1] in obj.keys():
attr = fcu_key[0].split('"')[1]
if 'attr' not in locals():
return [0]
#since blender 3 access to custom property settings changed
if attr in source:
if not isinstance(source[attr], float) and not isinstance(source[attr], int):
return [0]
id_attr = source.id_properties_ui(attr).as_dict()
attrvalue = id_attr['default']
return [attrvalue]
return [0]
def store_handles(key):
#storing the distance between the handles bezier to the key value
handle_r = key.handle_right[1] - key.co[1]
handle_l = key.handle_left[1] - key.co[1]
return handle_r, handle_l
def apply_handles(key, handle_r, handle_l):
key.handle_right[1] = key.co[1] + handle_r
key.handle_left[1] = key.co[1] + handle_l
def selected_bones_filter(obj, fcu_data_path):
if not bpy.context.window_manager.atb_ui.multikey.selectedbones:
#if not obj.als.onlyselected:
return False
if obj.mode != 'POSE':
return True
transform_types = ['location', 'rotation_euler', 'rotation_quaternion', 'scale']
#filter selected bones if option is turned on
bones = [bone.path_from_id() for bone in bpy.context.selected_pose_bones]
if fcu_data_path.split('].')[0]+']' not in bones and fcu_data_path not in transform_types:
return True
# def filter_properties(obj, fcu):
# 'Filter the W X Y Z attributes of the transform properties'
# transform = fcu.data_path.split('"].')[1] if obj.mode == 'POSE' else fcu.data_path
# index = fcu.array_index
# if 'rotation' in transform:
# if transform == 'rotation_euler':
# index -= 1
# transform = 'rotation'
# transform = 'filter_' + transform
# if not hasattr(bpy.context.scene.multikey, transform):
# return False
# attr = getattr(bpy.context.scene.multikey, transform)
# #print(fcu.data_path, index, transform, attr[index])
# return True if attr[index] else False
def add_value(key, value):
if key.select_control_point:
#store handle values in relative to the keyframe value
handle_r, handle_l = store_handles(key)
key.co[1] += value
apply_handles(key, handle_r, handle_l)
#calculate the difference between current value and the fcurve value
def add_diff(fcurves, path, current_value, eval_array):
array_value = current_value - eval_array
if not any(array_value):
return
for i, value in enumerate(array_value):
fcu = fcurves.find(path, index = i)
if fcu is None or Tools.filter_properties(bpy.context.scene.animtoolbox, fcu):
continue
for key in fcu.keyframe_points:
add_value(key, value)
fcu.update()
class ScaleValuesOp(bpy.types.Operator):
"""Modal operator used while scale value is running before release"""
bl_idname = "animtoolbox.multikey_scale_value"
bl_label = "Scale Values"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
#reset the values for dragging
self.stop = False
ui = context.window_manager.atb_ui
ui['is_dragging'] = True
self.avg_value = dict()
#dictionary of the keyframes and their original INITIAL values
self.keyframes_values = dict()
self.keyframes_handle_right = dict()
self.keyframes_handle_left = dict()
#the average value for each fcurve
self.keyframes_avg_value = dict()
for obj in context.selected_objects:
if obj.animation_data.action is None:
continue
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
for fcu in fcurves:
if obj.mode == 'POSE':
if selected_bones_filter(obj, fcu.data_path):
continue
if Tools.filter_properties(context.scene.animtoolbox, fcu):
continue
#avg and value list per fcurve
avg_value = []
value_list = []
for key in fcu.keyframe_points:
if key.select_control_point:
value_list.append(key.co[1])
self.keyframes_values.update({key : key.co[1]})
self.keyframes_handle_right.update({key : key.handle_right[1]})
self.keyframes_handle_left.update({key : key.handle_left[1]})
if len(value_list)>1:
#the average value with the scale property added to it
avg_value = sum(value_list) / len(value_list)
for key in fcu.keyframe_points:
if key.select_control_point:
self.keyframes_avg_value.update({key : avg_value})
if not self.keyframes_avg_value:
ui['is_dragging'] = False
ui.multikey['scale'] = 1
Tools.redraw_areas(['VIEW_3D'])
return {'CANCELLED'}
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
ui = context.window_manager.atb_ui
scale = ui.multikey.scale
#Quit the modal operator when the slider is released
if self.stop:
ui['is_dragging'] = False
ui.multikey['scale'] = 1
Tools.redraw_areas(['VIEW_3D'])
#modal is being cancelled because of undo issue with the modal running through the property
return {'FINISHED'}
if event.value == 'RELEASE': # Stop the modal on next frame. Don't block the event since we want to exit the field dragging
self.stop = True
return {'PASS_THROUGH'}
for key, key_value in self.keyframes_values.items():
if not key.select_control_point:
continue
if key not in self.keyframes_avg_value:
continue
avg_value = self.keyframes_avg_value[key]
handle_right_value = self.keyframes_handle_right[key]
handle_left_value = self.keyframes_handle_left[key]
#add the value of the distance from the average * scale factor
key.co[1] = avg_value + ((key_value - avg_value)*scale)
key.handle_right[1] = avg_value + ((handle_right_value - avg_value)*scale)
key.handle_left[1] = avg_value + ((handle_left_value - avg_value)*scale)
return {'PASS_THROUGH'}
def scale_value(self, context):
ui = context.window_manager.atb_ui
if ui.is_dragging:
return
obj = context.object
if obj is None:
self['scale'] = 1
return
action = obj.animation_data.action
if action is None:
self['scale'] = 1
return
if context.mode == 'POSE' and not context.selected_pose_bones:
self['scale'] = 1
return
bpy.ops.animtoolbox.multikey_scale_value('INVOKE_DEFAULT')
def random_value(self, context):
for obj in context.selected_objects:
if obj.animation_data.action is None:
continue
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
for fcu in fcurves:
if obj.mode == 'POSE':
if selected_bones_filter(obj, fcu.data_path):
continue
if Tools.filter_properties(context.scene.animtoolbox, fcu):
continue
value_list = []
threshold = bpy.context.window_manager.atb_ui.multikey.randomness
for key in fcu.keyframe_points:
if key.select_control_point == True:
value_list.append(key.co[1])
if len(value_list) > 0:
value = max(value_list)- min(value_list)
for key in fcu.keyframe_points:
add_value(key, value * random.uniform(-threshold, threshold))
fcu.update()
def evaluate_combine(data_path, added_array, eval_array, array_default, influence):
if 'scale' in data_path:
eval_array = eval_array * (added_array / array_default) ** influence
elif 'rotation_quaternion' in data_path:
#multiply first the influence with the w separatly
added_array[0] = added_array[0] + (1- added_array[0])*(1 - influence)
added_array[1:] *= influence
eval_array = np.array(Quaternion(eval_array) @ Quaternion(added_array))# ** influence
#if it's a custom property
elif 'rotation_euler' not in data_path and 'location' not in data_path:
eval_array = eval_array + (added_array - array_default) * influence
return eval_array
def evaluate_array(fcurves, fcu_path, frame, array_default = [0, 0, 0]):
'''Create an array from all the indexes'''
array_len = len(array_default)
fcu_array = []
#get the missing arrays in case quaternion is not complete
missing_arrays = []
for i in range(array_len):
fcu = fcurves.find(fcu_path, index = i)
if fcu is None:
missing_arrays.append(i)
continue
fcu_array.append(fcu.evaluate(frame))
#In case it's a quaternion and missing attributes, then adding from default value
if fcu_array and array_len == 4 and missing_arrays:
for i in missing_arrays:
fcu_array.insert(i, array_default[i])
if not len(fcu_array):
return None
return np.array(fcu_array)
def evaluate_layers(context, obj, anim_data, fcu, array_default):
'''Calculate the evaluation of all the layers when using the nla'''
if not hasattr(anim_data, 'nla_tracks') or not anim_data.use_nla:
return None
nla_tracks = anim_data.nla_tracks
if not len(nla_tracks):
return None
frame = context.scene.frame_current
blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
fcu_path = fcu.data_path
eval_array = array_default
for track in nla_tracks:
if track.mute:
continue
if not len(track.strips):
continue
for strip in track.strips:
if not strip.frame_start < frame < strip.frame_end:
continue
action = strip.action
if action is None:
continue
blend_type = strip.blend_type
#get the influence value either from the attribute or the fcurve. function coming from bake
influence = strip.influence
if len(strip.fcurves):
if not strip.fcurves[0].mute and len(strip.fcurves[0].keyframe_points):
influence = strip.fcurves[0].evaluate(frame)
#evaluate the frame according to the strip settings
frame_eval = frame
#change the frame if the strip is on hold
if frame < strip.frame_start:
if strip.extrapolation == 'HOLD':
frame_eval = strip.frame_start
elif frame >= strip.frame_end:
if strip.extrapolation == 'HOLD' or strip.extrapolation == 'HOLD_FORWARD':
frame_eval = strip.frame_end
last_frame = strip.frame_start + (strip.frame_end - strip.frame_start) / strip.repeat
if strip.repeat > 1 and (frame) >= last_frame:
action_range = (strip.action_frame_end * strip.scale - strip.action_frame_start * strip.scale)
frame_eval = (((frame_eval - strip.frame_start) % (action_range)) + strip.frame_start)
if strip.use_reverse:
frame_eval = last_frame - (frame_eval - strip.frame_start)
offset = (strip.frame_start * 1/strip.scale - strip.action_frame_start) * strip.scale
frame_eval = strip.frame_start * 1/strip.scale + (frame_eval - strip.frame_start) * 1/strip.scale - offset * 1/strip.scale
fcurves = Tools.get_fcurves_channelbag(obj, action)
eval_array = evaluate_blend_type(fcurves, eval_array, fcu_path, frame_eval, influence, array_default, blend_type, blend_types)
#Adding an extra layer from the action outside and on top of the nla
tweak_mode = anim_data.use_tweak_mode
if tweak_mode:
anim_data.use_tweak_mode = False
action = anim_data.action
if action:
influence = anim_data.action_influence
blend_type = anim_data.action_blend_type
fcurves = Tools.get_fcurves_channelbag(obj, action)
eval_array = evaluate_blend_type(fcurves, eval_array, fcu_path, frame, influence, array_default, blend_type, blend_types)
anim_data.use_tweak_mode = tweak_mode
return eval_array
def evaluate_blend_type(fcurves, eval_array, fcu_path, frame, influence,
array_default, blend_type, blend_types):
'''Calculate the value based on the blend type'''
fcu_array = evaluate_array(fcurves, fcu_path, frame, array_default)
if fcu_array is None:
return eval_array
###EVALUATION###
if blend_type =='COMBINE':
if 'location' in fcu_path or 'rotation_euler' in fcu_path:
blend_type = 'ADD'
if blend_type =='REPLACE':
eval_array = eval_array * (1 - influence) + fcu_array * influence
elif blend_type =='COMBINE':
eval_array = evaluate_combine(fcu_path, fcu_array, eval_array, array_default, influence)
else:
eval_array = eval('eval_array' + blend_types[blend_type] + 'fcu_array' + '*' + str(influence))
return eval_array
def evaluate_value(self, context):
ui = context.window_manager.atb_ui
for obj in context.selected_objects:
anim_data = obj.animation_data
if anim_data is None:
return
if anim_data.action is None:
return
action = obj.animation_data.action
fcu_paths = []
transformations = ["rotation_quaternion","rotation_euler", "location", "scale"]
if obj.mode == 'POSE':
bonelist = context.selected_pose_bones if ui.multikey.selectedbones else obj.pose.bones
fcurves = Tools.get_fcurves_channelbag(obj, action)
for fcu in fcurves:
if fcu in fcu_paths:
continue
if Tools.filter_properties(context.scene.animtoolbox, fcu):
continue
if obj.mode == 'POSE':
if selected_bones_filter(obj, fcu.data_path):
continue
for bone in bonelist:
#find the fcurve of the bone
if fcu.data_path.rfind(bone.name) != 12 or fcu.data_path[12 + len(bone.name)] != '"':
continue
path_split = fcu.data_path.split('"].')
if len(path_split) <= 1:
continue
else:
transform = fcu.data_path.split('"].')[1]
if transform not in transformations:
continue
current_value = getattr(obj.pose.bones[bone.name], transform)
else:
transform = fcu.data_path
current_value = getattr(obj, transform)
array_default = np.array(attr_default(obj, (fcu.data_path, fcu.array_index)))
# array_default = np.array([attr_default(obj, (fcu.data_path, i)) for i in range(4)
# if fcurves.find(fcu.data_path, index = i) is not None])
eval_array = evaluate_layers(context, obj, anim_data, fcu, array_default)
if eval_array is None:
fcurves = Tools.get_fcurves_channelbag(obj, action)
eval_array = evaluate_array(fcurves, fcu.data_path, context.scene.frame_current, array_default)
#calculate the difference between current value and the fcurve value
add_diff(fcurves, fcu.data_path, np.array(current_value), eval_array)
class MULTIKEY_OT_Multikey(bpy.types.Operator):
"""Edit all selected keyframes"""
bl_label = "Edit Selected Keyframes"
bl_idname = "animtoolbox.multikey"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.active_object and context.active_object.animation_data and bpy.context.scene.tool_settings.use_keyframe_insert_auto == False
def execute(self, context):
evaluate_value(self, context)
return {'FINISHED'}
class MultikeyProperties(bpy.types.PropertyGroup):
selectedbones: bpy.props.BoolProperty(name="Selected Bones", description="Affect only selected bones", default=True, options={'HIDDEN'})
handletype: bpy.props.BoolProperty(name="Keep handle types", description="Keep handle types", default=False, options={'HIDDEN'})
scale: bpy.props.FloatProperty(name="Scale Factor", description="Scale percentage of the average value", default=1.0, update = scale_value)
randomness: bpy.props.FloatProperty(name="Randomness", description="Random Threshold of keyframes", default=0.1, min=0.0, max = 1.0, update = random_value)
#filters
# filter_location: bpy.props.BoolVectorProperty(name="Location", description="Filter Location properties", default=(True, True, True), size = 3, options={'HIDDEN'})
# filter_rotation: bpy.props.BoolVectorProperty(name="Rotation", description="Filter Rotation properties", default=(True, True, True, True), size = 4, options={'HIDDEN'})
# filter_scale: bpy.props.BoolVectorProperty(name="Scale", description="Filter Scale properties", default=(True, True, True), size = 3, options={'HIDDEN'})
# class FilterProperties(bpy.types.Operator):
# """Filter Location Rotation and Scale Properties"""
# bl_idname = "fcurves.filter"
# bl_label = "Filter Properties W X Y Z"
# bl_options = {'REGISTER', 'UNDO'}
# def invoke(self, context, event):
# 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 = 'Location')
# row.prop(context.scene.multikey, 'filter_location', text = '')
# row = layout.row()
# row.label(text = 'Rotation')
# row.prop(context.scene.multikey, 'filter_rotation', text = '')
# row = layout.row()
# row.label(text = 'Scale')
# row.prop(context.scene.multikey, 'filter_scale', text = '')
# def execute(self, context):
# return {'CANCELLED'}
classes = (ScaleValuesOp, MULTIKEY_OT_Multikey)
#register, unregister = bpy.utils.register_classes_factory(classes)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
# bpy.types.Scene.animtoolbox.multikey = bpy.props.PointerProperty(type = MultikeyProperties, options={'LIBRARY_EDITABLE'}, override = {'LIBRARY_OVERRIDABLE'})
def unregister():
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)
# del bpy.types.Scene.animtoolbox.multikey
+700
View File
@@ -0,0 +1,700 @@
# ***** 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 . import Rigger_Toolbox
from . import multikey
from . import Rigger_Toolbox
from pathlib import Path
from bpy.utils import register_class
from bpy.utils import unregister_class
import os
######################################################## MENUS ########################################################
class ANIMTOOLBOX_MT_Copy_Paste_Matrix(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Copy_Paste_Matrix'
bl_label = "Copy Paste Matrix"
@classmethod
def poll(cls, context):
return True
def draw(self, context):
layout = self.layout
layout.operator('anim.copy_matrix', text = 'Copy World Matrix', icon ='DUPLICATE')
layout.operator('anim.paste_matrix', text = 'Paste World Matrix', icon ='PASTEDOWN')
layout.separator()
layout.operator('anim.copy_relative_matrix', text = 'Copy Relative Matrix', icon ='DUPLICATE')
layout.operator('anim.paste_relative_matrix', text = 'Paste Relative Matrix', icon ='PASTEDOWN')
layout.separator()
layout.prop_menu_enum(context.scene.animtoolbox, 'range_type', text = 'Frame Range Type')
if context.scene.animtoolbox.range_type == 'RANGE':
# split = layout.split(factor = 0.3)
# layout.prop(context.scene.animtoolbox, 'bake_frame_start', text = 'Start')
# layout.prop(context.scene.animtoolbox, 'bake_frame_end', text = 'End')
layout.operator("anim.markers_bakerange", icon = 'MARKER', text ='Frame Range Markers Widget', depress = context.scene.animtoolbox.bake_frame_range)
class ANIMTOOLBOX_MT_Temp_Ctrls(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Temp_Ctrls'
bl_label = "Temp Ctrls"
@classmethod
def poll(cls, context):
return True
def draw(self, context):
layout = self.layout
custom_icons = preview_collections["main"]
layout.operator('anim.bake_to_ctrl', text="World Space Ctrls", icon = 'WORLD')
layout.operator('anim.bake_to_temp_fk', text="Temp FK Ctrls", icon = 'BONE_DATA')
layout.operator('anim.bake_to_temp_ik', text="Temp IK Ctrls", icon = 'CON_KINEMATIC')
layout.separator()
layout.operator('anim.link_temp_chains', text="Link Temp Chains", icon = 'DECORATE_LINKED')
layout.operator('anim.unlink_temp_chains', text="UnLink Temp Chains", icon = 'DECORATE_LINKED')
layout.separator()
layout.operator('anim.bake_temp_ctrls', text="Bake Temp Ctrls", icon_value = custom_icons["oven"].icon_id)
layout.operator('anim.remove_bones_constraints', text="Cleanup", icon_value = custom_icons["trash"].icon_id)
layout.prop_menu_enum(context.scene.btc, 'target', text = 'Bake To', icon = 'MOD_ARMATURE')
layout.separator()
ctrl = context.object.animtoolbox.controller if context.object.animtoolbox.controller else context.object
if ctrl:
layout.prop(ctrl.animtoolbox, 'ctrls_enabled', text ='Temp Ctrls On/Off')
# layout.operator('anim.enable_tempctrls', text="On" , icon = 'HIDE_OFF')#icon = 'CONSTRAINT_BONE'
# layout.operator('anim.disable_tempctrls', text="Off", icon = 'HIDE_ON')
layout.separator()
layout.prop_menu_enum(context.scene.btc, 'shape_type', icon = 'MESH_DATA')
layout.prop(context.scene.btc, 'shape_size', slider = False, icon ='CON_SIZELIKE')
layout.prop(context.scene.btc, 'color_set', text = '')
# layout.prop(context.scene.btc, 'shape_type', icon = 'MESH_DATA', text ='')
class ANIMTOOLBOX_MT_Keyframe_Offset(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Keyframe_Offset'
bl_label = "Interactive Keyframe Offset"
def draw(self, context):
layout = self.layout
layout.prop(context.scene.animtoolbox, 'keyframes_offset', slider = False)
layout.operator('anim.select_keyframes_offset', text="Select Offset Keyframes", icon ='RESTRICT_SELECT_OFF')
layout.operator('anim.apply_keyframes_offset', text="Apply Offset", icon = 'ANIM')
class ANIMTOOLBOX_MT_Blendings(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Blendings'
bl_label = "Blendings"
def draw(self, context):
layout = self.layout
ui = context.window_manager.atb_ui
layout.prop(ui, 'inbetween_worldmatrix', slider = True)
layout.prop(context.scene.animtoolbox, 'inbetweener', slider = True)
layout.prop(ui, 'blend_mirror', slider = True)
class ANIMTOOLBOX_MT_Multikey(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Multikey'
bl_label = "Multikey"
def draw(self, context):
layout = self.layout
ui = context.window_manager.atb_ui
layout.operator("animtoolbox.multikey", icon = 'ACTION_TWEAK')
layout.prop(ui.multikey, 'scale', slider = True)
layout.prop(ui.multikey, 'randomness', slider = True)
class ANIMTOOLBOX_MT_Convert_Rotations(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_Convert_Rotations'
bl_label = "Convert Rotations"
def draw(self, context):
layout = self.layout
layout.operator("anim.convert_rotation_mode", icon = 'DRIVER_ROTATIONAL_DIFFERENCE', text = 'Convert Rotation To')
layout.operator("anim.find_rotation_mode", icon = 'VIEWZOOM', text = 'Recommend Euler Rotation')
layout.prop_menu_enum(context.scene.animtoolbox, 'rotation_mode', text = 'Convert To')
class ANIMTOOLBOX_MT_operators(bpy.types.Menu):
bl_idname = 'ANIMTOOLBOX_MT_menu_operators'
bl_label = "AnimToolBox"
@classmethod
def poll(cls, context):
return True
def draw(self, context):
layout = self.layout
scene = context.scene
ui = context.window_manager.atb_ui
custom_icons = preview_collections["main"]
if context.area.type == 'VIEW_3D':
layout.menu('ANIMTOOLBOX_MT_Temp_Ctrls', text = 'Temp Ctrls', icon_value = custom_icons["puppet"].icon_id)
layout.separator()
layout.operator('anim.share_keyframes', text = 'Share Keyframes', icon_value = custom_icons["sharekeys"].icon_id)
layout.operator('anim.relative_cursor', text = 'Relative Cursor Pivot', icon_value = custom_icons["relative_cursor"].icon_id, depress = ui.relative_cursor)
layout.operator("anim.markers_retimer", icon_value = custom_icons["retime"].icon_id, text ='Markers Retimer', depress = ui.markers_retimer)
layout.menu('ANIMTOOLBOX_MT_Convert_Rotations', text = 'Convert Rotations', icon = 'DRIVER_ROTATIONAL_DIFFERENCE')
layout.separator()
layout.menu('ANIMTOOLBOX_MT_Keyframe_Offset', text = 'Interactive Keyframe Offset', icon_value = custom_icons["keyframe_offset"].icon_id)
layout.menu('ANIMTOOLBOX_MT_Blendings', text = 'Blendings and inbetweens', icon_value = custom_icons["sliders"].icon_id)
layout.separator()
layout.menu('ANIMTOOLBOX_MT_Copy_Paste_Matrix', text = 'Copy Paste Matrix', icon_value = custom_icons["copy_matrix"].icon_id)
if bpy.context.preferences.addons[__package__].preferences.multikey:
layout.separator()
layout.menu('ANIMTOOLBOX_MT_Multikey', text = 'Multikey', icon_value = custom_icons["multikey"].icon_id)
layout.separator(factor = 0.5)
layout.operator('anim.switch_collections_visibility', icon = 'COLLECTION_COLOR_06', text = 'Animated Collections Visibilty')
layout.operator('anim.isolate_pose_mode', icon_value = custom_icons["isolate"].icon_id, depress = scene.animtoolbox.isolate_pose_mode)
layout.operator('object.motion_path_operator', text = 'Editable Motion Path', depress = scene.animtoolbox.motion_path, icon_value = custom_icons["mt"].icon_id)
layout.separator()
layout.prop(context.preferences.addons[__package__].preferences, 'quick_menu', text = 'Use Quick Icons Menu')
# layout.prop(context.window_manager.atb_ui, 'quick_menu', text = 'Use Quick Icons Menu')
# elif context.area.type == 'DOPESHEET_EDITOR':
# layout.operator('wm.call_menu_pie', text="Pie AnimOffset").name = 'ANIMAIDE_MT_pie_anim_offset'
# layout.menu('ANIMAIDE_MT_anim_offset')
# layout.menu('ANIMAIDE_MT_anim_offset_mask')
# elif context.area.type == 'GRAPH_EDITOR':
# layout.menu('ANIMAIDE_MT_curve_tools_pie')
######################################################## WorkSpaceTools ########################################################
class KeyframeOffsetTool(bpy.types.WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'POSE'
bl_idname = 'animtoolbox.keyframe_offset'
bl_label = 'Keyframe Offset Tool'
bl_description = (
'Offset the keyframes of the selected bone\n'
'Shift + Click to select the bones with an offset\n'
'Ctrl + Alt + Click to Apply the offset'
)
bl_icon = (Path(__file__).parent / "icons" / "ops.anim.keyframe_offset").as_posix()
bl_keymap = (
('anim.keyframe_offset', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG'}, None),
# ('anim.select_keyframes_offset', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'shift': True}, None),
('anim.apply_keyframes_offset', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'ctrl': True, 'alt': True}, None),
('view3d.select_box', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG', 'shift': True}, {'properties': [('mode', 'ADD')]}),
('view3d.select_box', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG', 'ctrl': True}, {'properties': [('mode', 'SUB')]}),
('view3d.select_box', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG', 'shift': True, 'ctrl': True}, {'properties': [('mode', 'AND')]}),
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK'}, {'properties': [('deselect_all', True)]}),
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'shift': True}, {'properties': [('toggle', True)]}),
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'alt': True}, {'properties': [('enumerate', True)]}),
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'shift': True, 'alt': True}, {'properties': [('toggle', True), ('enumerate', True)]})
)
######################################################## PANELS ########################################################
class ANIMTOOLBOX_PT_Panel:
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Animation"
#bl_options = {"DEFAULT_CLOSED"}
@classmethod
def poll(cls, context):
return context.object is not None
class ANIMTOOLBOX_PT_MainPanel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
#bl_category = "Animation"
bl_label = "AnimToolBox"
bl_idname = "ANIMTOOLBOX_PT_MainPanel"
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
custom_icons = preview_collections["main"]
self.layout.label(text='', icon_value = custom_icons["animtoolbox"].icon_id)
def draw(self, context):
obj = context.object
if obj is None:
return
# layout = self.layout
# row = layout.row(align = True)
# row.label(text = 'Support me on ')
# row.operator("wm.url_open", text="Patreon", icon = 'FUND').url = "https://www.patreon.com/animtoolbox"
class TEMPCTRLS_PT_Panel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
#bl_category = "Animation"
bl_label = "Temp Controls"
bl_idname = "TEMPCTRLS_PT_Panel"
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
custom_icons = preview_collections["main"]
self.layout.label(text="", icon_value = custom_icons["puppet"].icon_id)
def draw(self, context):
obj = context.object
btc = context.scene.btc
if obj is None:
return
custom_icons = preview_collections["main"]
oven_icon = custom_icons["oven"]
# row = layout.row()
ui = context.window_manager.atb_ui
layout = self.layout
box = layout.box()
col = box.column()
col.operator('anim.bake_to_ctrl', text="WorldSpace Ctrls", icon ='WORLD') #
col.operator('anim.bake_to_temp_fk', text="Temp FK Ctrls", icon = 'BONE_DATA')
col.operator('anim.bake_to_temp_ik', text="Temp IK Ctrls", icon = 'CON_KINEMATIC')
# layout.separator(factor = 0.1)
# box = layout.box()
row = box.row()
row.operator('anim.link_temp_chains', text="Link Temp Chains", icon = 'DECORATE_LINKED')
row.prop(btc, 'linksettings', text = '', icon = 'SETTINGS')
if btc.linksettings:
# insidebox = box.box()
row = box.row()
row.label(text = 'Link to Active Chain: ')
row.prop(btc, 'link_to', text = '')
row = box.row()
row.operator('anim.unlink_temp_chains', text="UnLink Temp Chains", icon = 'UNLINKED')
# row = insidebox.row()
# row.label(text = 'Link from Chain: ')
# row.prop(btc, 'link_from', text = '')
# layout.separator(factor = 0.1)
box = layout.box()
row = box.row()
row.operator('anim.bake_to_empties', text="WorldSpace Empties")
row.operator('anim.empties_to_bones', text="", icon = 'EMPTY_AXIS')
box = layout.box()
row = box.row()
# row.operator('anim.bake_constrained_bones', text="Quick Bake", icon = 'REC')
row.operator('anim.bake_temp_ctrls', text="Bake Temp Ctrls", icon_value = oven_icon.icon_id)
row.prop(btc, 'bakesettings', text = '', icon = 'SETTINGS')
if btc.bakesettings:
insidebox = box.box()
# split = insidebox.split(factor = 0.9)
# split.prop(btc, 'smartbake', text = 'Smart Bake')
# split.operator('fcurves.filter_ui', icon ='FILTER', text = '')
# if btc.smartbake:
# row.prop(btc, 'inbetween_keyframes')
split = insidebox.split(factor = 0.3)
split.label(text = 'Bake To:')
# split.split(factor = 0.9)
split_2 = split.split(factor = 0.9)
split_2.prop(btc, 'target', text = '')
split_2.operator('fcurves.filter_ui', icon ='FILTER', text = '')
split = insidebox.split(factor = 0.3)
split.label(text = 'Bake Range:')
split.prop(btc, 'bake_range_type', text = '')
if btc.bake_range_type == 'KEYFRAMES':
row = insidebox.row()
row.label(text = 'Keyframes: ')
row.prop(btc, 'smartbake', text = 'Smart Bake')
row.prop(btc, 'bake_layers', text = 'All Layers')
row = insidebox.row()
row.label(text = 'From:')
row.prop(btc, 'from_origin', text = 'Origin')
row.prop(btc, 'from_ctrl', text = 'Ctrls')
elif btc.bake_range_type == 'CUSTOM':
row = insidebox.row()
row.prop(btc, 'bake_range', text = '')
insidebox.separator(factor=0.1)
row = insidebox.row()
row.label(text = 'Clean: ')
row.prop(btc, 'clean_constraints', text = 'Constraints')
if btc.clean_constraints:
row.prop(btc, 'clean_ctrls', text = 'Ctrls')
row = box.row()
row.operator('anim.remove_bones_constraints', text="Cleanup", icon_value = custom_icons["trash"].icon_id)
row.prop(btc, 'cleansettings', text = '', icon = 'SETTINGS')
if btc.cleansettings:
insidebox = box.box()
split = insidebox.split(factor = 0.4)
split.label(text = 'Target: ')
split.prop(btc, 'target', text = '')
row = insidebox.row()
row.label(text = 'Clean: ')
row.prop(btc, 'clean_constraints', text = 'Constraints')
if btc.clean_constraints:
row.prop(btc, 'clean_ctrls', text = 'Ctrls')
if btc.target == 'SELECTED':
split = insidebox.split(factor = 0.68)
split.label(text = 'ReBake Link Ctrls to Original: ')
split.prop(btc, 'rebake_to_org', text = '')
box = layout.box()
row = box.row()
# row = layout.row()
# row.prop(obj.animtoolbox, 'influence', text = 'Influence', slider = True)
ctrl = obj.animtoolbox.controller if obj.animtoolbox.controller else obj
if ctrl:
enabled_icon = 'HIDE_OFF' if ctrl.animtoolbox.ctrls_enabled else 'HIDE_ON'
row.prop(ctrl.animtoolbox, 'ctrls_enabled', text ='Temp Ctrls On/Off', icon = enabled_icon)
# row = box.row()
# row.prop(ui, 'temp_ctrls_switch', icon = 'RESTRICT_VIEW_OFF', text = '')
# row.label(text = 'Temp Ctrls Switch: ')
# row.prop(btc, 'target', text = '', icon = 'MOD_ARMATURE', icon_only = True)
# if ui.temp_ctrls_switch:
# # split = layout.split(factor = 0.225)
# row = box.row()
# row.operator('anim.enable_tempctrls', text="On" , icon = 'HIDE_OFF')#icon = 'CONSTRAINT_BONE'
# row.operator('anim.disable_tempctrls', text="Off", icon = 'HIDE_ON')
box.separator(factor=0.1)
row = box.row()
row.prop(ui, 'temp_ctrls_shapes', icon = 'MESH_DATA', text = '')
row.label(text = 'Temp Ctrls Shapes: ')
row.prop(btc, 'target', text = '', icon = 'MOD_ARMATURE', icon_only = True)
if ui.temp_ctrls_shapes:
row = box.row()
row.prop(btc, 'shape_size', slider = False)
row.prop(btc, 'shape_type', text = '')
row = layout.row()
# split.label(text = 'Theme Color: ')
row.prop(btc, 'color_set', text = '')
layout.separator()
row = layout.row()
row.operator('anim.btc_selections', text="Select", icon = 'CONSTRAINT_BONE')
row.prop(btc, 'selection', text = '', icon = 'CON_ARMATURE')
class ANIMTOOLBOX_PT_Tools(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
bl_label = "Anim Tools"
bl_idname = "ANIMTOOLBOX_PT_Tools"
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
custom_icons = preview_collections["main"]
layout = self.layout
layout.label(text="", icon_value = custom_icons["toolbox"].icon_id)
def draw(self, context):
obj = context.object
scene = context.scene
atb = scene.animtoolbox
if obj is None:
return
custom_icons = preview_collections["main"]
layout = self.layout
ui = context.window_manager.atb_ui
row = layout.row()
filter_text = 'Filter Tools Properties' if atb.filter_name == '' else 'Filters: ' + atb.filter_name
filter_depress = False if atb.filter_name == '' else True
row.operator('fcurves.filter_ui', icon ='FILTER', text = filter_text, depress = filter_depress)
layout.separator()
#layout.operator('anim.keyframes_offset', text="Offset Keyframes", icon = 'NEXT_KEYFRAME')
split = layout.split(factor = 0.9)
split.prop(context.scene.animtoolbox, 'keyframes_offset', slider = True)
split.operator('anim.apply_keyframes_offset', text="", icon_value = custom_icons["apply"].icon_id)
split.operator('anim.select_keyframes_offset', text="", icon_value = custom_icons["select"].icon_id)
row = layout.row()
row.operator('anim.share_keyframes', text = 'Share Keyframes', icon_value = custom_icons["sharekeys"].icon_id)
row = layout.row()
row.operator("anim.relative_cursor", text ='Relative Cursor Pivot', icon_value = custom_icons["relative_cursor"].icon_id, depress = ui.relative_cursor)
row = layout.row()
row.operator("anim.markers_retimer", icon_value = custom_icons["retime"].icon_id, text ='Markers Retimer', depress = ui.markers_retimer)
layout.separator(factor = 0.5)
box = layout.box()
# icon = 'DOWNARROW_HLT' if ui.copy_paste_matrix is True else 'RIGHTARROW_THIN'
# split = box.split(factor = 0.9)
# split.prop(ui, 'copy_paste_matrix', icon_value = custom_icons["copy_matrix"].icon_id, text = 'Copy Paste Matrices')
# split.operator('fcurves.filter', icon ='FILTER', text = '')
row = box.row()
row.prop(ui, 'copy_paste_matrix', icon_value = custom_icons["copy_matrix"].icon_id, text = 'Copy Paste Matrices')
if ui.copy_paste_matrix:
row = box.row()
row.label(text = 'Copy World Matrix :', icon = 'WORLD')
row.operator('anim.copy_matrix', text = '', icon ='DUPLICATE')
row.operator('anim.paste_matrix', text = '', icon ='PASTEDOWN')
row = box.row()
row.label(text = 'Copy Relative Matrix :', icon = 'LINKED')
row.operator('anim.copy_relative_matrix', text = '', icon ='DUPLICATE')
row.operator('anim.paste_relative_matrix', text = '', icon ='PASTEDOWN')
split = box.split(factor = 0.32)
split.label(text = 'Paste To')
split.prop(context.scene.animtoolbox, 'range_type', text = '')
if context.scene.animtoolbox.range_type == 'RANGE':
row = box.row()
row.prop(context.scene.animtoolbox, 'bake_frame_start', text = 'Start')
row.prop(context.scene.animtoolbox, 'bake_frame_end', text = 'End')
row.operator("anim.markers_bakerange", icon = 'MARKER', text ='', depress = context.scene.animtoolbox.bake_frame_range)
layout.separator(factor = 0.5)
box = layout.box()
# icon = 'DOWNARROW_HLT' if ui.Inbetweens is True else 'RIGHTARROW_THIN'
# split = box.split(factor = 0.9)
# split.prop(ui, 'Inbetweens', icon_value = custom_icons["sliders"].icon_id, text = 'Blendings / Inbetweens')
# split.operator('fcurves.filter', icon ='FILTER', text = '')
row = box.row()
row.prop(ui, 'Inbetweens', icon_value = custom_icons["sliders"].icon_id, text = 'Blendings / Inbetweens')
if ui.Inbetweens:
col = box.column()
col.prop(ui, 'inbetween_worldmatrix', slider = True)
col.prop(scene.animtoolbox, 'inbetweener', slider = True)
col.prop(ui, 'blend_mirror', slider = True)
layout.separator(factor = 1)
# my_icon = custom_icons["rotations"]
split = layout.split(factor = 0.6)
split.operator("anim.convert_rotation_mode", icon = 'DRIVER_ROTATIONAL_DIFFERENCE', text = 'Convert Rotation To')
# split.operator("anim.convert_rotation_mode", text = 'Convert Rotation To', icon_value = my_icon.icon_id)
split = split.split(factor = 0.3)
split.operator("anim.find_rotation_mode", icon = 'VIEWZOOM', text = '')
split.prop(context.scene.animtoolbox, 'rotation_mode', text = '')
class MULTIKEY_PT_Panel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
"""Add random value to selected keyframes"""
bl_label = "Multikey"
bl_idname = "MULTIKEY_PT_Panel"
# bl_space_type = 'VIEW_3D'
# bl_region_type = 'UI'
# bl_category= 'Animation'
bl_parent_id = 'ANIMTOOLBOX_PT_Tools'
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
custom_icons = preview_collections["main"]
layout = self.layout
layout.label(text="", icon_value = custom_icons["multikey"].icon_id)
def draw(self, context):
custom_icons = preview_collections["main"]
layout = self.layout
ui = context.window_manager.atb_ui
# layout.separator()
split = layout.split(factor=0.1, align = True)
split.prop(ui.multikey, 'selectedbones', icon_value = custom_icons["selected_bones"].icon_id, text = '')
# split = split.split(factor=0.1, align = True)
#split.operator('fcurves.filter', icon ='FILTER', text = '')
split.operator("animtoolbox.multikey", icon = 'ACTION_TWEAK')
layout.separator()
row = layout.row(align = True)
row.prop(ui.multikey, 'scale')
row.prop(ui.multikey, 'randomness', slider = True)
class ANIMTOOLBOX_PT_Display(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
bl_label = "Display"
bl_idname = "ANIMTOOLBOX_PT_Display"
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
bl_options = {"DEFAULT_CLOSED"}
def draw_header(self, context):
custom_icons = preview_collections["main"]
self.layout.label(text="", icon_value = custom_icons["display"].icon_id)
def draw(self, context):
scene = context.scene
layout = self.layout
ui = context.window_manager.atb_ui
custom_icons = preview_collections["main"]
box = layout.box()
row = box.row()
icon = 'DOWNARROW_HLT' if ui.gizmo_size is True else 'RIGHTARROW_THIN'
row.prop(ui, 'gizmo_size', icon = icon, text = 'Add to Gizmo Size:')
if ui.gizmo_size:
row = box.row()
row.prop(scene.animtoolbox, 'gizmo_size', text = '')
row.operator('view3d.gizmo_size_up', text="", icon ='ADD')
row.operator('view3d.gizmo_size_down', text="", icon ='REMOVE')
layout.separator(factor = 0.5)
row = layout.row()
col_vis_icon = 'OUTLINER_COLLECTION' if scene.animtoolbox.col_vis else 'COLLECTION_COLOR_06'
row.operator('anim.switch_collections_visibility', icon = col_vis_icon, text = 'Animated Collections Visibilty', depress = scene.animtoolbox.col_vis)
layout.separator(factor = 0.5)
row = layout.row()
row.operator('anim.isolate_pose_mode', icon_value = custom_icons["isolate"].icon_id, depress = scene.animtoolbox.isolate_pose_mode)
layout.separator(factor = 0.5)
box = layout.box()
row = box.row()
row.operator('object.motion_path_operator', text = 'Editable Motion Path', depress = scene.animtoolbox.motion_path, icon_value = custom_icons["mt"].icon_id)
row.prop(scene.animtoolbox, 'mp_settings', text = '', icon = 'SETTINGS')
if scene.animtoolbox.mp_settings:
row = box.row()
row.prop(scene.animtoolbox, 'mp_color_before', text = '')
row.prop(scene.animtoolbox, 'mp_color_after', text = '')
row = box.row()
row.prop(scene.animtoolbox, 'mp_keyframe_scale', text = 'Scale Keyframes Box', icon = 'CUBE')
row = box.row()
row.prop(scene.animtoolbox, 'mp_points', text = 'Points')
row.prop(scene.animtoolbox, 'mp_lines', text = 'Lines')
row = box.row()
row.prop(scene.animtoolbox, 'mp_handles', text = 'Handles')
row.prop(scene.animtoolbox, 'mp_infront', text = 'In Front')
row = box.row()
row.prop(scene.animtoolbox, 'mp_display_frames', text = 'Display Keyframe Numbers')
row = box.row()
row.prop(context.preferences.addons[__package__].preferences, 'keyframes_range', text = 'Keyframe Distance Range')
if context.object is None:
return
split = box.split(factor = 0.4)
split.label(text ='Frame Range')
# row.prop(scene.animtoolbox, 'mp_frame_range', text = '')
split.prop(context.scene.animtoolbox, 'mp_frame_range', text = '')
if context.scene.animtoolbox.mp_frame_range == 'MANUAL':
row = box.row()
row.prop(context.scene.animtoolbox, 'bake_frame_start', text = 'Start')
row.prop(context.scene.animtoolbox, 'bake_frame_end', text = 'End')
row.operator("anim.markers_bakerange", icon = 'MARKER', text ='', depress = scene.animtoolbox.bake_frame_range)
elif context.scene.animtoolbox.mp_frame_range == 'AROUND':
row = box.row()
row.prop(context.scene.animtoolbox, 'mp_before', text = 'Before')
row.prop(context.scene.animtoolbox, 'mp_after', text = 'After')
# row.operator("anim.markers_bakerange", icon = 'MARKER', text ='', depress = scene.animtoolbox.bake_frame_range)
class RIGGERTOOLBOX_PT_Panel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
bl_label = "Rigger Toolbox"
bl_idname = "RIGGERTOOLBOX_PT_Panel"
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
bl_options = {"DEFAULT_CLOSED"}
def draw(self, context):
obj = context.object
if obj is None:
return
layout = self.layout
col = layout.column()
col.operator("armature.add_bbone_widgets", text = "Add Bbone Widgets", icon = 'IPO_BEZIER')
col.separator()
col.operator("armature.add_chain_ctrls", text = "Add Chain Controls", icon = 'OUTLINER_DATA_ARMATURE')
col.separator()
col.operator("armature.merge", text = "Merge Rigs", icon = 'MOD_ARMATURE')
# Add-ons Preferences Update Panel
# Define Panel classes for updating
panels = [ANIMTOOLBOX_PT_MainPanel, TEMPCTRLS_PT_Panel, ANIMTOOLBOX_PT_Tools, ANIMTOOLBOX_PT_Display]
def update_panel(self, context):
message = "Animtoolbox: Updating Panel locations has failed"
try:
for panel in panels:
if "bl_rna" in panel.__dict__:
unregister_class(panel)
for panel in panels:
panel.bl_category = context.preferences.addons[__package__].preferences.category
register_class(panel)
except Exception as e:
print("\n[{}]\n{}\n\nError:\n{}".format(__package__, message, e))
pass
def add_multikey(self, context):
if bpy.context.preferences.addons[__package__].preferences is None:
return
if bpy.context.preferences.addons[__package__].preferences.multikey:
multikey.register()
register_class(MULTIKEY_PT_Panel)
register_class(ANIMTOOLBOX_MT_Multikey)
panels.append(MULTIKEY_PT_Panel)
#panels = panels + (MULTIKEY_PT_Panel)
elif hasattr(bpy.types, 'MULTIKEY_PT_Panel'):
multikey.unregister()
panels.remove(MULTIKEY_PT_Panel)
unregister_class(MULTIKEY_PT_Panel)
unregister_class(ANIMTOOLBOX_MT_Multikey)
def add_riggertoolbox(self, context):
if bpy.context.preferences.addons[__package__].preferences is None:
return
if bpy.context.preferences.addons[__package__].preferences.riggertoolbox:
Rigger_Toolbox.register()
register_class(RIGGERTOOLBOX_PT_Panel)
panels.append(RIGGERTOOLBOX_PT_Panel)
#panels = panels + (RIGGERTOOLBOX_PT_Panel)
elif hasattr(bpy.types, 'RIGGERTOOLBOX_PT_Panel'):
Rigger_Toolbox.unregister()
panels.remove(RIGGERTOOLBOX_PT_Panel)
unregister_class(RIGGERTOOLBOX_PT_Panel)
classes = (ANIMTOOLBOX_MT_Copy_Paste_Matrix, ANIMTOOLBOX_MT_Temp_Ctrls, ANIMTOOLBOX_MT_operators, ANIMTOOLBOX_MT_Keyframe_Offset,
ANIMTOOLBOX_MT_Blendings, ANIMTOOLBOX_MT_Convert_Rotations) + tuple(panels)
preview_collections = {}
#list of all the custom icons
icons_list = {'relative_cursor' : 'relative_cursor.png', 'puppet' : 'puppet.png', 'animtoolbox' : 'animtoolbox.png',
'sliders' : 'slider-navigation.png', 'world_space' : 'earth-rotation.png', 'isolate' : 'isolation.png',
'mt' : 'curve.png', 'toolbox' : 'tools.png', 'oven' : 'oven.png', 'retime' : 'retime.png', 'copy_matrix' : 'copy_world.png',
'sharekeys' : 'sharekeys.png', 'sliders' : 'sliders.png', 'keyframe_offset' : 'keyframes_offset.png', 'display' : 'display.png',
'switch' : 'switch.png', 'trash' : 'trash.png', 'apply' : 'apply.png', 'select' : 'select.png', 'worldspace' : 'WorldSpace.png',
'multikey' : 'multikey.png', 'selected_bones' : 'selected_bones.png'}
def register_custom_icon():
# Note that preview collections returned by bpy.utils.previews
# are regular py objects - you can use them to store custom data.
import bpy.utils.previews
custom_icons = bpy.utils.previews.new()
# path to the folder where the icon is
# the path is calculated relative to this py file inside the addon folder
icons_dir = os.path.join(os.path.dirname(__file__), "icons")
# load a preview thumbnail of a file and store in the previews collection
for iconname, icon_file in icons_list.items():
custom_icons.load(iconname, os.path.join(icons_dir, icon_file), 'IMAGE')
preview_collections["main"] = custom_icons
def draw_menu(self, context):
if context.mode == 'OBJECT' or context.mode == 'POSE':
layout = self.layout
scene = context.scene
ui = context.window_manager.atb_ui
custom_icons = preview_collections["main"]
layout.menu('ANIMTOOLBOX_MT_menu_operators') #, icon_value = custom_icons["toolbox"].icon_id, text =''
if not context.preferences.addons[__package__].preferences.quick_menu:
return
#Only Icons Menu
temp_ctrls = custom_icons["puppet"]
layout.menu('ANIMTOOLBOX_MT_Temp_Ctrls' , icon_value = temp_ctrls.icon_id, text ='')
# layout.separator()
layout.menu('ANIMTOOLBOX_MT_Copy_Paste_Matrix' , icon_value = custom_icons["copy_matrix"].icon_id, text ='')
layout.separator()
layout.operator('anim.relative_cursor', text = '', depress = ui.relative_cursor, icon_value = custom_icons["relative_cursor"].icon_id)
layout.separator()
layout.operator('anim.markers_retimer', text = '', depress = ui.markers_retimer, icon_value = custom_icons["retime"].icon_id)
layout.separator()
layout.operator('anim.isolate_pose_mode', text = '', depress = scene.animtoolbox.isolate_pose_mode, icon_value = custom_icons["isolate"].icon_id)
layout.separator()
layout.operator('object.motion_path_operator', text = '', depress = scene.animtoolbox.motion_path, icon_value = custom_icons["mt"].icon_id)