2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
File diff suppressed because it is too large Load Diff
+42 -18
View File
@@ -1803,7 +1803,7 @@ def reorder_bones_matrices(bones_matrices, constrained):
return re_bones_matrices
def paste_bone_matrix(bone, matrix_copied, constrained, x_filter = True):
def paste_bone_matrix(bone, matrix_copied, constrained, bones = {}, x_filter = True):
#running again separatly in case the bones are in a hierarchy and influencing each other
# for bone, matrix_copied in bones_matrices.items():
# Determine whether to use bone.matrix or bone.matrix_world
@@ -1818,10 +1818,14 @@ def paste_bone_matrix(bone, matrix_copied, constrained, x_filter = True):
if x_filter : matrix_copied = filter_matrix_properties(context, getattr(bone, matrix_attr), matrix_copied)
# bone.matrix = bone.id_data.matrix_world.inverted() @ matrix_copied
setattr(bone, matrix_attr, matrix_copied) # bone.id_data.matrix_world.inverted() @
children = set(bone.children_recursive).intersection(bones)
if children or bone in constrained:
# print(f'found children {[child.name for child in children]} in bone {bone.name}' )
context.view_layer.update()
#Check if the bone has constrainsts on it that need extra iteration
if bone not in constrained:
# filter_matrix_properties(context, bone.matrix, matrix_copied)
return
context.view_layer.update()
matrix_copied = reverse_bone_constraints(context, bone, matrix_copied)
if x_filter : matrix_copied = filter_matrix_properties(context, bone.matrix, matrix_copied)
@@ -1832,8 +1836,12 @@ def paste_bone_matrix(bone, matrix_copied, constrained, x_filter = True):
def paste_bones_matrices(bones_matrices, constrained, x_filter = True):
#running again separatly in case the bones are in a hierarchy and influencing each other
pasted_bones = set()
for bone, matrix_copied in bones_matrices.items():
paste_bone_matrix(bone, matrix_copied, constrained, x_filter)
#Get the rest of the bones to check if they are children of the current bone
bones = set(bones_matrices.keys()).difference(pasted_bones)
paste_bone_matrix(bone, matrix_copied, constrained, bones, x_filter)
pasted_bones.add(bone)
class PasteRelativeMatrix(bpy.types.Operator):
"""paste the relative matrix of the selection"""
@@ -2522,7 +2530,15 @@ class ConvertRotationMode(bpy.types.Operator):
# @classmethod
# def poll(cls, context):
# return context.object.type == 'ARMATURE'
def switch_rot_mode_keyframes(self, obj, posebone):
# Switching any rotation mode keyframes to the new rotation mode value using to_rot_mode_index
bone_path = posebone.path_from_id() + '.' if type(posebone) == bpy.types.PoseBone else ''
fcurves = get_fcurves_channelbag(obj, obj.animation_data.action)
fcu_rotation_mode = fcurves.find(data_path = bone_path + 'rotation_mode', index = 0)
if fcu_rotation_mode:
for keyframe in fcu_rotation_mode.keyframe_points:
keyframe.co[1] = self.to_rot_mode_index
def execute(self, context):
scene = context.scene
selected_bones = context.selected_pose_bones
@@ -2530,7 +2546,8 @@ class ConvertRotationMode(bpy.types.Operator):
to_rot_mode = scene.animtoolbox.rotation_mode
to_rot_mode_fcu = rot_mode_to_channel(to_rot_mode)
#Getting the index of the rotation mode we want to convert to
self.to_rot_mode_index = list(scene.animtoolbox.bl_rna.properties['rotation_mode'].enum_items.keys()).index(to_rot_mode)
#get the keyframes from the bones
for posebone in selected_bones:
@@ -2548,16 +2565,21 @@ class ConvertRotationMode(bpy.types.Operator):
keyframes = emp.get_bone_keyframes(posebone, transform)
#get all interpolations and handle types of the keyframes
handle_types = emp.get_bone_keyframes(posebone, transform, property = 'interpolation')
interpolations = handle_types[::3]
handle_left_type = handle_types[1::3]
handle_right_type = handle_types[2::3]
# rotation_mode_keyframes = emp.get_bone_keyframes(posebone, 'rotation_mode')
self.switch_rot_mode_keyframes(obj, posebone)
inbetweens = []
smartframes = sorted(set(map(lambda x: round(x, 2), keyframes[::2])))
inbetweens = add_inbetweens(smartframes)
all_frames = sorted(smartframes + inbetweens)
#get all interpolations and handle types of the keyframes
if len(smartframes) > 1:
handle_types = emp.get_bone_keyframes(posebone, transform, property = 'interpolation')
interpolations = handle_types[::3]
handle_left_type = handle_types[1::3]
handle_right_type = handle_types[2::3]
inbetweens = add_inbetweens(smartframes)
all_frames = sorted(smartframes + inbetweens)
new_path = posebone.path_from_id() + '.' + to_rot_mode_fcu
#define array length
@@ -2632,13 +2654,15 @@ class ConvertRotationMode(bpy.types.Operator):
keyframe = new_fcu.keyframe_points[-1]
keyframe.co = (frame, fcu_keyframes[new_fcu][frame])
keyframe.interpolation = interpolations[frame_index]
keyframe.handle_left_type = handle_left_type[frame_index]
keyframe.handle_right_type = handle_right_type[frame_index]
if inbetweens:
keyframe.interpolation = interpolations[frame_index]
keyframe.handle_left_type = handle_left_type[frame_index]
keyframe.handle_right_type = handle_right_type[frame_index]
new_fcu.update()
add_interpolations(new_fcurves, fcu_inbetweens)
if inbetweens:
add_interpolations(new_fcurves, fcu_inbetweens)
posebone.rotation_mode = to_rot_mode
+64 -57
View File
@@ -20,7 +20,7 @@
bl_info = {
"name": "AnimToolBox",
"author": "Tal Hershkovich",
"version" : (0, 0, 7, 3),
"version" : (0, 0, 8),
"blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel",
"description": "A set of animation tools",
@@ -133,15 +133,16 @@ class TempCtrlsBoneSettings(bpy.types.PropertyGroup):
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)])
('WORLDSPACE_CURSOR', 'World Space Cursor Ctrl','World Space Cursor pivot', 2),
('TEMPFK', 'Temporary FK setup','Temporary FK chain setup', 3),
('TEMPFK_FLIP', 'Temporary flipped FK setup','Temporary flipped FK chain setup', 4),
('TEMPIK', 'Temporary IK setup','Temporary IK setup', 5),
('POLE', 'Temporary IK Pole setup','Temporary IK Pole', 6),
('PARENTCTRL', 'Parent Ctrl from cursor setup','Parent Ctrl from cursor setup', 7),
('ROOT', 'Root', 'Root Ctrl for all the setups', 8),
('EMPTY', 'Root', 'Root Ctrl for all the setups', 9),
('TRACK_TO', 'Track To','World Space Track to Ctrl setup', 10),
('TRACK_TO_EMPTY', 'Track To Empty','World Space Track to Empty Ctrl setup', 11)])
#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'},
@@ -152,21 +153,29 @@ class TempCtrlsBoneSettings(bpy.types.PropertyGroup):
shape: bpy.props.BoolProperty(name = "Apply shape", description = "Mark if the bone needs a shape", default = False, override = {'LIBRARY_OVERRIDABLE'})
class TempCtrlsOrgIds(bpy.types.PropertyGroup):
# A collection of the org ids used in each setup. Org ID is the direct connection
# between original bones and ctrls
pass
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),
])
('WORLDSPACE_CURSOR', 'World Space Cursor Ctrl','World Space Cursor pivot', 2),
('TEMPFK', 'Temporary FK setup','Temporary FK chain setup', 3),
('TEMPFK_FLIP', 'Temporary flipped FK setup','Temporary flipped FK chain setup', 4),
('TEMPIK', 'Temporary IK setup','Temporary IK setup', 5),
('POLE', 'Temporary IK Pole setup','Temporary IK Pole', 6),
('PARENTCTRL', 'Parent Ctrl from cursor setup','Parent Ctrl from cursor setup', 7),
('ROOT', 'Root', 'Root Ctrl for all the setups', 8),
('EMPTY', 'Root', 'Root Ctrl for all the setups', 9),
('TRACK_TO', 'Track To','World Space Track to Ctrl setup', 10),
('TRACK_TO_EMPTY', 'Track To Empty','World Space Track to Empty Ctrl setup', 11)])
org_ids: bpy.props.CollectionProperty(type = TempCtrlsOrgIds)
class MultikeyProperties(bpy.types.PropertyGroup):
@@ -238,37 +247,6 @@ class AnimToolBoxGlobalSettings(bpy.types.PropertyGroup):
#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
@@ -312,6 +290,13 @@ class AnimToolBoxPreferences(bpy.types.AddonPreferences):
#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)
mp_pref: bpy.props.BoolProperty(name = "Editable Motion Path Colors Theme", description = "Set the Color them of editable motion path visualization", default = False)
mp_keyframe_color: bpy.props.FloatVectorProperty(name="Keyframes", subtype='COLOR', default=(1.0, 1.0, 0.0, 1.0), size=4, min=0.0, max=1.0, description="Handles selection color")
mp_handle_color: bpy.props.FloatVectorProperty(name="Handles", subtype='COLOR', default=(1.0, 0.8, 0.2, 1.0), size=4, min=0.0, max=1.0, description="Handles selection color")
mp_remove_color: bpy.props.FloatVectorProperty(name="Remove Keyframes", subtype='COLOR', default=(0.0, 0.5, 1.0, 1.0), size=4, min=0.0, max=1.0, description="Keyframe color displayed before removing")
mp_hover_color: bpy.props.FloatVectorProperty(name="Hover", subtype='COLOR', default=(1.0, 0.4, 0.2, 1.0), size=4, min=0.0, max=1.0, description="Color during Hovering")
mp_handle_selection_color: bpy.props.FloatVectorProperty(name="Handles Selection", subtype='COLOR', default=(0.8, 0.65, 0.6, 0.8), size=4, min=0.0, max=1.0, description="Handles selection color")
mp_key_selection_color: bpy.props.FloatVectorProperty(name="Keyframe Selection", subtype='COLOR', default=(0.8, 0.8, 0.6, 0.8), size=4, min=0.0, max=1.0, description="Keyframe selection color")
# addon updater preferences from `__init__`, be sure to copy all of them
auto_check_update: bpy.props.BoolProperty(
@@ -371,6 +356,22 @@ class AnimToolBoxPreferences(bpy.types.AddonPreferences):
row.prop(self, 'clear_setup')
row.prop(self, 'in_front')
layout.separator()
box = layout.box()
col = box.column()
col.prop(self, 'mp_pref', icon = 'DOWNARROW_HLT', text = 'Editable Motion Path Preferences')
if self.mp_pref:
col.prop(self, 'keyframes_range', text = 'Keyframe Distance Range')
col.label(text = 'Colors Theme')
row = box.row()
row.prop(self, 'mp_keyframe_color')
row.prop(self, 'mp_handle_color')
row.prop(self, 'mp_remove_color')
row = box.row()
row.prop(self, 'mp_hover_color')
row.prop(self, 'mp_key_selection_color')
row.prop(self, 'mp_handle_selection_color')
layout.separator()
col = layout.column()
col.label(text = 'Include Extras: ')
@@ -385,12 +386,14 @@ def loadanimtoolbox_pre(self, context):
if scene.animtoolbox.bake_frame_range:
scene.animtoolbox.bake_frame_range = False
if scene.animtoolbox.motion_path:
scene.animtoolbox.motion_path = False
if scene.emp.motion_path:
scene.emp.motion_path = False
bpy.context.workspace.status_text_set(None)
if 'mp_dh' in dns:
bpy.types.SpaceView3D.draw_handler_remove(dns['mp_dh'], 'WINDOW')
bpy.app.driver_namespace.pop('mp_dh')
bpy.context.scene.emp.selected_keyframes = '{}'
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')
@@ -411,16 +414,20 @@ def loadanimtoolbox_post(self, context):
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 scene.emp.motion_path:
scene.emp.motion_path = False
bpy.context.workspace.status_text_set(None)
if 'mp_dh' in dns:
bpy.types.SpaceView3D.draw_handler_remove(dns['mp_dh'], 'WINDOW')
bpy.app.driver_namespace.pop('mp_dh')
# Reset keyframe selection for motion paths it is not used in window manager
# Because it is used for undo
bpy.context.scene.emp.selected_keyframes = '{}'
Tools.selection_order(self, context)
classes = (TempCtrlsItems, TempCtrlsObjectSetups, TempCtrlsSceneSettings,TempCtrlsBoneSettings, MultikeyProperties, IsolatedRigs,
AnimToolBoxObjectSettings, AnimToolBoxUILayout, AnimToolBoxGlobalSettings) + ui.classes
classes = (TempCtrlsItems, TempCtrlsOrgIds, TempCtrlsObjectSetups, TempCtrlsSceneSettings,TempCtrlsBoneSettings, MultikeyProperties,
IsolatedRigs, AnimToolBoxObjectSettings, AnimToolBoxUILayout, AnimToolBoxGlobalSettings) + ui.classes
addon_keymaps = []
@@ -1,6 +1,6 @@
{
"last_check": "2025-06-19 15:30:39.178174",
"backup_date": "June-19-2025",
"last_check": "2025-09-23 10:58:29.201165",
"backup_date": "September-23-2025",
"update_ready": false,
"ignore": false,
"just_restored": false,
@@ -2101,6 +2101,9 @@ def smartbake(context, org_action, posebones, ids):
for fcu in fcurves:
if path not in fcu.data_path:
continue
#apply the bake only to the transformation channels
if fcu.data_path.split('"].')[-1] not in transformations:
continue
if Tools.filter_properties(context.scene.animtoolbox, fcu):
continue
if posebone.btc.setup in {'TRACK_TO', 'TRACK_TO_EMPTY'} and 'rotation' not in fcu.data_path:
@@ -2108,6 +2111,8 @@ def smartbake(context, org_action, posebones, ids):
if scene.btc.from_origin:
smartfcurve = get_smartfcurve(fcu, smartfcus, posebone, fcu.data_path, fcu.array_index, scene.btc.smartbake)
if smartfcurve is None:
continue
#if it's not the original action then create a new fcurve in org_action where it's baked to and assign it to smartfcurve
#Usefull especially when working inside a new empty layer
if action != org_action and (fcu.data_path, fcu.array_index) not in org_action_fcus:
@@ -2201,7 +2206,7 @@ def smartbake(context, org_action, posebones, ids):
smartfcurve.modifiers.append(mod)
#Remove frames to not include in the bake, in case of custom frame range
if frames_remove:
if frames_remove and smartfcurve:
smartframes = {frame for frame in smartframes if round(frame) not in frames_remove}
smartfcurve.frames = [frame for frame in smartfcurve.frames if round(frame) not in frames_remove]
smartfcurve.interpolations = {frame : interpolation for frame, interpolation in smartfcurve.interpolations.items() if round(frame) not in frames_remove}
@@ -2367,6 +2372,7 @@ def remove_constraints(context, ids, controllers, controlled_objs):
ctrls = list(controllers) + [ctrl.children[0] for ctrl in controllers if ctrl.children]
scene = context.scene
for obj in controlled_objs:
if obj.type != 'ARMATURE':
continue
@@ -2378,16 +2384,17 @@ def remove_constraints(context, ids, controllers, controlled_objs):
#check if the bone has anything assigned to it
if not bone.btc.setup_id:
continue
#reset the setup_id
bone.btc.setup_id = 0
bone.btc.org_id = 0
bone.btc.org = bone.btc.setup ='NONE'
for con in bone.constraints:
if not hasattr(con, 'target'):
continue
if con.target not in ctrls:
continue
#reset the setup_id
bone.btc.setup_id = 0
bone.btc.org_id = 0
bone.btc.org = bone.btc.setup ='NONE'
con_path = con.path_from_id()
#Removing target because of a bug in Blender 4.4, otherwise it crashes when switching mode
con.target = None
@@ -2481,6 +2488,13 @@ def get_controllers_controlled(context):
controlled_objs = {item.controlled for item in scene.btc.ctrl_items if item.controlled}
controllers = {item.controller for item in scene.btc.ctrl_items if item.controller}
#In case controlled objs are not found and there are still bones with ids
#The add the object of this bones to reset them
if not controlled_objs and context.selected_pose_bones:
for bone in context.selected_pose_bones:
if bone.btc.setup_id or bone.btc.org_id:
controlled_objs.add(bone.id_data)
return controllers, controlled_objs
def unhide_org(obj):
@@ -2652,9 +2666,9 @@ def get_bone_ids(context, id = 'setup_id'):
if select_filter == 'ALL':
#get ids from All the controller rigs and bones
# org_ids = {bone.btc.org_id for obj in scene.objects if obj.animtoolbox.controlled for bone in obj.pose.bones if bone.btc.org_id}
for obj in scene.objects:
if not obj.animtoolbox.controlled:
if not obj.animtoolbox.controlled and obj not in context.selected_objects:
#still checking selected objects in case animtoolbox.controlled is empty
continue
if obj.type == 'ARMATURE':
for bone in obj.pose.bones:
@@ -2663,11 +2677,6 @@ def get_bone_ids(context, id = 'setup_id'):
elif obj.type == 'EMPTY' and id == 'setup_id':
if getattr(obj.animtoolbox, id):
ids.add(getattr(obj.animtoolbox, id))
# ids = {getattr(bone.btc, id) for obj in scene.objects if obj.animtoolbox.controlled and obj.type == 'ARMATURE'
# for bone in obj.pose.bones if getattr(bone.btc, id)}
# if id == 'setup_id':
# ids = {getattr(obj.animtoolbox, id) for obj in scene.objects
# if obj.animtoolbox.controlled and getattr(obj.animtoolbox, id)}
return ids
@@ -2814,14 +2823,7 @@ def rebake_connection_ctrls(self, context, obj, rebake_bones, parent, root_name
# constraints_clear(ctrls)
#remove extra channels from the temporary ctrls before removing them
action = obj.animation_data.action
ctrl_paths = [ctrl.path_from_id() for ctrl in ctrls]
fcurves = Tools.get_fcurves_channelbag(obj, action)
for fcu in fcurves:
path = fcu.data_path.split('"].')[0] + ('"]')
if path in ctrl_paths:
fcurves.remove(fcu)
remove_tempctrls_channels(obj, ctrls)
#removing the temporary ctrls
bpy.ops.object.mode_set(mode = 'EDIT')
@@ -2840,6 +2842,22 @@ def rebake_connection_ctrls(self, context, obj, rebake_bones, parent, root_name
return
def remove_tempctrls_channels(obj, ctrls):
#remove extra channels from the temporary ctrls before removing them
if not obj.animation_data:
return
if not obj.animation_data.action:
return
action = obj.animation_data.action
ctrl_paths = [ctrl.path_from_id() for ctrl in ctrls]
fcurves = Tools.get_fcurves_channelbag(obj, action)
for fcu in fcurves:
path = fcu.data_path.split('"].')[0] + ('"]')
if path in ctrl_paths:
fcurves.remove(fcu)
def get_rebake_bones(rig, ids):
#get all the bones with connection (Ctrls and their children)
rebake_bones = []
@@ -2967,8 +2985,8 @@ class Cleanup(bpy.types.Operator):
scene = context.scene
ids = get_bone_ids(context)
controllers, controlled_objs = get_controllers_controlled(context)
if not scene.btc.clean_constraints:
return {'FINISHED'}
#in case the controllers are empties add their ids and if the root is selected
@@ -2977,7 +2995,6 @@ class Cleanup(bpy.types.Operator):
continue
ids.add(ctrl.animtoolbox.setup_id)
controllers_remove_child(ctrl, controllers)
remove_constraints(context, ids, controllers, controlled_objs)
if not scene.btc.clean_ctrls:
@@ -1573,7 +1573,8 @@ class PasteMatrix(bpy.types.Operator):
if bone.id_data.name in objs_matrix:
if bone.name in objs_matrix[bone.id_data.name]:
matrix_copied = objs_matrix[bone.id_data.name][bone.name]
matrix_copied = obj.matrix_world.inverted() @ matrix_copied
#Store the matrices for each bone that will use it
matrix_copied = reverse_childof_constraint(bone, matrix_copied, constrained)
@@ -1662,11 +1663,11 @@ class CopyRelativeMatrix(bpy.types.Operator):
#create a dictionary for all the realtive distance of the bones and objects
objs_matrix_dist = dict()
#get the source bofne or object
#get the source bone or object
if context.active_pose_bone:
source_active = context.active_pose_bone
source_rig_name = source_active.id_data.name
source_matrix = source_active.matrix
source_matrix = source_active.matrix
else:
source_active = context.active_object
source_matrix = source_active.matrix_world
@@ -1681,8 +1682,14 @@ class CopyRelativeMatrix(bpy.types.Operator):
continue
if bone_relative == source_active:
continue
matrix_dist = source_matrix.inverted() @ obj.matrix_world @ bone_relative.matrix# @ obj.matrix_world
#Adding the offset from the armature transform both for the active and relative
rig_offset = obj.matrix_world
if source_active.id_data.type == 'ARMATURE':
rig_offset = source_active.id_data.matrix_world.inverted() @ rig_offset
matrix_dist = source_matrix.inverted() @ rig_offset @ bone_relative.matrix
#store each bone matrix distance in a dictionary
if obj.name in objs_matrix_dist:
objs_matrix_dist[obj.name].update({bone_relative.name : matrix_dist})
@@ -1735,41 +1742,59 @@ def reverse_childof_constraint(source, matrix_source, constrained = set()):
offsets_lerp = []
offset_inv = Matrix.Identity(4)
offset_inv_lerp = Matrix.Identity(4)
#If the source is a bone then get the Armature matrix to add to the calculation
if type(source) == bpy.types.PoseBone:
obj_offset = obj.matrix_world
else:
obj_offset = Matrix.Identity(4)
#iterate and store all the inverted offsets of all the child constraints
for con in source.constraints:
if con.mute or not con.influence:
continue
if hasattr(con, 'target') and con.target is None:
continue
if con.type != 'CHILD_OF':
constrained.add(source)
continue
parent_matrix = con.target.matrix_world
if con.subtarget != '':
if con.subtarget == '':
parent_matrix = obj_offset.inverted() @ con.target.matrix_world
#remove obj.matrix_world when connected to an object
offset = parent_matrix @ con.inverse_matrix - Matrix.Identity(4)
#offset for the scale with influence already included
offset_lerp = Matrix.Identity(4).lerp(parent_matrix @ con.inverse_matrix, con.influence)
else:
parent_matrix = con.target.pose.bones[con.subtarget].matrix
offsets.append(Matrix.Identity(4) + con.influence * (parent_matrix @ con.inverse_matrix - Matrix.Identity(4)))
offsets_lerp.append(Matrix.Identity(4).lerp(parent_matrix @ con.inverse_matrix, con.influence))
if con.target != obj:
parent_matrix = obj_offset.inverted() @ con.target.matrix_world @ parent_matrix
#Include armature object matrix
offset = parent_matrix @ con.inverse_matrix - Matrix.Identity(4)
offset_lerp = Matrix.Identity(4).lerp(parent_matrix @ con.inverse_matrix, con.influence)
#Adding the influence to the offset
offset = Matrix.Identity(4) + con.influence * offset
offset_inv = offset_inv @ offset.inverted()
offset_inv_lerp = offset_inv_lerp @ offset_lerp.inverted()
offsets.append(offset)
if not offsets:
return matrix_source #@ obj.matrix_world.inverted()
return matrix_source
#Multiply all the child constraint inverted offsets
for offset, offsets_lerp in zip(offsets, offsets_lerp):
#offset_inv = offset_inv @ parent_offset_inv
offset_inv = offset_inv @ offset.inverted()
offset_inv_lerp = offset_inv_lerp @ offsets_lerp.inverted()
# add object space if it's a bone
if source != obj:
offset_inv = offset_inv #@ obj.matrix_world.inverted()
#final Matrix values
matrix_basis = offset_inv @ matrix_source
matrix_lerp = offset_inv_lerp @ matrix_source
matrix_lerp = offset_inv_lerp @ matrix_source
loc, rot, scale = matrix_basis.decompose()
loc_lerp, rot_lerp, scale_lerp = matrix_lerp.decompose()
matrix_basis = Matrix.LocRotScale(loc, rot_lerp, scale_lerp)
return matrix_basis
return matrix_basis
def reorder_bones_matrices(bones_matrices, constrained):
#Reordering the bones, so that we apply first the matrix offset to the constrained bones
@@ -1837,9 +1862,11 @@ class PasteRelativeMatrix(bpy.types.Operator):
if 'source_rig_name' in globals():
if source_rig_name in bpy.data.objects:
source_rig = bpy.data.objects[source_rig_name]
source_obj = source_rig
source_bone = source_rig.pose.bones[source_active_name]
#Get the current matrix of the source bone
matrix_source = source_bone.matrix
elif 'source_active_name' in globals():
if source_active_name in bpy.data.objects:
source_obj = bpy.data.objects[source_active_name]
@@ -1854,10 +1881,14 @@ class PasteRelativeMatrix(bpy.types.Operator):
#if the source object was in object mode during copy and now it's pose mode then quit
frame_range, inbetweens = get_frame_range(context, obj)
fcu_inbetweens = dict()
for frame in sorted(frame_range+inbetweens):
scene.frame_set(int(frame))
if obj.mode == 'POSE':
for bone in context.selected_pose_bones:
if bone.id_data != obj:
continue
#check that the selected bone is not the source bone
if bone == source_bone:
continue
@@ -1868,14 +1899,18 @@ class PasteRelativeMatrix(bpy.types.Operator):
if bone.name in objs_matrix_dist[bone.id_data.name]:
bone_matrix_dist = objs_matrix_dist[bone.id_data.name][bone.name]
matrix_new = matrix_source @ bone_matrix_dist
#Adding the offset from the armature transform both for the active and relative
#If it's the same Armature it will cancel each other
rig_offset = obj.matrix_world.inverted()
if source_rig:
rig_offset = rig_offset @ source_rig.matrix_world
matrix_new = rig_offset @ matrix_source @ bone_matrix_dist
#Store the matrices for each bone that will use it
matrix_new = reverse_childof_constraint(bone, matrix_new, constrained)
bones_matrices.update({bone : matrix_new})
matrix_copied = reverse_childof_constraint(bone, matrix_new, constrained)
# if bone not in constrained:
# matrix_copied = filter_matrix_properties(context, bone.matrix, matrix_copied)
#Reordering the bones, so that we apply first the matrix offset to the constrained bones
bones_matrices = reorder_bones_matrices(bones_matrices, constrained)
@@ -1896,16 +1931,16 @@ class PasteRelativeMatrix(bpy.types.Operator):
else:
obj_matrix_dist = matrix_dist
matrix_new = matrix_source @ obj_matrix_dist
matrix_copied = reverse_childof_constraint(target, matrix_new)
matrix_new = matrix_source @ obj_matrix_dist
matrix_new = reverse_childof_constraint(target, matrix_new)
if target not in constrained:
matrix_copied = filter_matrix_properties(context, target.matrix_world, matrix_copied)
target.matrix_world = matrix_copied
matrix_new = filter_matrix_properties(context, target.matrix_world, matrix_new)
target.matrix_world = matrix_new
if target in constrained:
context.view_layer.update()
matrix_copied = reverse_constraint_offset(target.matrix_world, matrix_copied)
target.matrix_world = filter_matrix_properties(context, target.matrix_world, matrix_copied)
matrix_new = reverse_constraint_offset(target.matrix_world, matrix_new)
target.matrix_world = filter_matrix_properties(context, target.matrix_world, matrix_new)
paste_keyframes_get_inbetweens(scene, target, inbetweens, frame, frame_range, fcu_inbetweens)
@@ -20,7 +20,7 @@
bl_info = {
"name": "AnimToolBox",
"author": "Tal Hershkovich",
"version" : (0, 0, 7, 1),
"version" : (0, 0, 7, 3),
"blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel",
"description": "A set of animation tools",
@@ -1,17 +1,16 @@
{
"last_check": "2025-06-19 15:30:39.178174",
"backup_date": "",
"last_check": "2025-09-23 10:57:14.918240",
"backup_date": "June-19-2025",
"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",
"link": "https://gitlab.com/api/v4/projects/45739913/repository/archive.zip?sha=cb445f00491a54eb763f0d8e72eaae3e44e7d0ba",
"version": [
0,
0,
7,
3
8
]
}
}
File diff suppressed because it is too large Load Diff
+16 -16
View File
@@ -46,7 +46,7 @@ def attr_default(obj, fcu_key):
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
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
@@ -273,6 +273,9 @@ def random_value(self, context):
for key in fcu.keyframe_points:
add_value(key, value * random.uniform(-threshold, threshold))
fcu.update()
self['randomness'] = 0.1
def evaluate_combine(data_path, added_array, eval_array, array_default, influence):
@@ -293,23 +296,17 @@ 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 = []
#assigning the default array in case
fcu_array = array_default.copy()
#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[i] = fcu.evaluate(frame)
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):
if (fcu_array == array_default).all():
return None
return np.array(fcu_array)
@@ -325,7 +322,7 @@ def evaluate_layers(context, obj, anim_data, fcu, array_default):
blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
fcu_path = fcu.data_path
eval_array = array_default
eval_array = array_default.copy()
for track in nla_tracks:
if track.mute:
@@ -373,6 +370,7 @@ def evaluate_layers(context, obj, anim_data, fcu, array_default):
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
@@ -425,6 +423,7 @@ def evaluate_value(self, context):
for fcu in fcurves:
if fcu in fcu_paths:
continue
current_value = None
if Tools.filter_properties(context.scene.animtoolbox, fcu):
continue
if obj.mode == 'POSE':
@@ -447,10 +446,11 @@ def evaluate_value(self, context):
else:
transform = fcu.data_path
current_value = getattr(obj, transform)
#In case it was completly filtered out and not current value available
if not current_value:
continue
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)
+59 -34
View File
@@ -63,6 +63,7 @@ class ANIMTOOLBOX_MT_Temp_Ctrls(bpy.types.Menu):
layout = self.layout
custom_icons = preview_collections["main"]
layout.operator('anim.bake_to_ctrl', text="World Space Ctrls", icon = 'WORLD')
# layout.operator('anim.worldspace_cursor', text="World Space Cursor Ctrls", icon ='ORIENTATION_CURSOR')
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()
@@ -163,7 +164,7 @@ class ANIMTOOLBOX_MT_operators(bpy.types.Menu):
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.operator('object.motion_path_operator', text = 'Editable Motion Path', depress = scene.emp.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')
@@ -257,7 +258,10 @@ class TEMPCTRLS_PT_Panel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
layout = self.layout
box = layout.box()
col = box.column()
col.operator('anim.bake_to_ctrl', text="WorldSpace Ctrls", icon ='WORLD') #
row = col.row()
row.operator('anim.bake_to_ctrl', text="WorldSpace Ctrls", icon ='WORLD') #
row.operator('anim.add_empty_ctrl', text="", icon ='EMPTY_AXIS')
# col.operator('anim.worldspace_cursor', text="WorldSpace Cursor Ctrls", icon ='ORIENTATION_CURSOR')
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')
@@ -540,44 +544,65 @@ class ANIMTOOLBOX_PT_Display(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
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
row.operator('object.motion_path_operator', text = 'Editable Motion Path', depress = scene.emp.motion_path, icon_value = custom_icons["mt"].icon_id)
emp = scene.emp
row.prop(scene.emp, 'settings', text = '', icon = 'SETTINGS')
if scene.emp.settings:
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':
split.prop(emp, 'frame_range', text = '')
if emp.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.prop(emp, 'frame_start', text = 'Start')
row.prop(emp, 'frame_end', text = 'End')
# row.operator("anim.markers_bakerange", icon = 'MARKER', text ='', depress = scene.animtoolbox.bake_frame_range)
elif emp.frame_range == 'AROUND':
row = box.row()
row.prop(emp, 'before', text = 'Before')
row.prop(emp, 'after', text = 'After')
box.separator(factor = 0.1)
col = box.column()
col.prop(emp, 'display_size', icon = 'DOWNARROW_HLT')
if emp.display_size:
col.prop(emp, 'thickness', text = 'Line Thickness')
col.prop(emp, 'keyframe_size', text = 'Keyframes Size')
col.prop(emp, 'frame_size', text = 'Frames Size')
box.separator(factor = 0.1)
row = box.row()
row.label(text = 'Visualization Type')
row.prop(emp, 'vis_type', text = '')
if emp.vis_type == 'VELOCITY':
row = box.row()
row.prop(emp, 'velocity_factor', text = '')
row.prop(emp, 'clamp_min', text = '')
row.prop(emp, 'clamp_max', text = '')
row = box.row()
row.prop(emp, 'color_before', text = '')
row.prop(emp, 'color_after', text = '')
row = box.row()
row.prop(emp, 'points', text = 'Points')
row.prop(emp, 'lines', text = 'Lines')
row = box.row()
row.prop(emp, 'handles', text = 'Handles')
row.prop(emp, 'infront', text = 'In Front')
row = box.row()
row.prop(emp, 'display_frames', text = 'Display Keyframe Numbers')
row.separator()
row = box.row()
row.operator('anim.go_to_keyframe')
# row = box.row()
# row.prop(context.preferences.addons[__package__].preferences, 'keyframes_range', text = 'Keyframe Distance Range')
# if context.object is None:
# return
# 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"
@@ -697,4 +722,4 @@ def draw_menu(self, context):
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)
layout.operator('object.motion_path_operator', text = '', depress = scene.emp.motion_path, icon_value = custom_icons["mt"].icon_id)