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
+42 -19
View File
@@ -20,11 +20,10 @@
bl_info = {
"name": "Animation Layers",
"author": "Tal Hershkovich",
"version" : (2, 1, 8, 8),
"version" : (2, 3, 4),
"blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel",
"description": "Simplifying the NLA editor into an animation layers UI and workflow",
#"warning": "New Branch of Animation Layers, Do not use below Blender v3.2",
"wiki_url": "",
"category": "Animation"}
@@ -66,7 +65,11 @@ class AnimLayersSceneSettings(bpy.types.PropertyGroup):
]
)
viewlayer_objects: bpy.props.IntProperty(name='View Layer Objects', description='checking if objects were turned on or off from the view Layers', default=0)
influence: bpy.props.FloatProperty(name="Layer Influence", description="Layer Influence", min = 0.0, options={'ANIMATABLE'}, max = 1.0, default = 1.0, precision = 3, update = anim_layers.influence_update)
influence_settings: bpy.props.BoolProperty(name="Influence Settings", description="Opens Influence settings menu", default=False)
influence_global: bpy.props.BoolProperty(name="Influence Global/Local", description="Influence options affect current layer or all layers", default=False)
edit_all_layers_op: bpy.props.BoolProperty(name="Edit All Layers Check Property", description="Operator to check if edit all layers is running", default=False)
class AnimLayersSettings(bpy.types.PropertyGroup):
turn_on: bpy.props.BoolProperty(name="Turn Animation Layers On", description="Turn on and start Animation Layers", default=False, options={'HIDDEN'}, update = anim_layers.turn_animlayers_on, override = {'LIBRARY_OVERRIDABLE'})
@@ -75,7 +78,7 @@ class AnimLayersSettings(bpy.types.PropertyGroup):
#Bake settings
smartbake: bpy.props.BoolProperty(name="Smart Bake", description="Stay with the same amount of keyframes after merging and baking", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
onlyselected: bpy.props.BoolProperty(name="Only selected Bones", description="Bake only selected Armature controls", default=True, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
onlyselected: bpy.props.BoolProperty(name="Only selected Bones", description="Bake only selected Armature controls", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
clearconstraints: bpy.props.BoolProperty(name="Clear constraints", description="Clear constraints during bake", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
mergefcurves: bpy.props.BoolProperty(name="Merge Cyclic & Fcurve modifiers", description="Include Fcurve modifiers in the bake", default = True, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
@@ -102,22 +105,27 @@ class AnimLayersSettings(bpy.types.PropertyGroup):
auto_blend: bpy.props.BoolProperty(name="Auto Blend", description="Apply blend type automatically based on scale and rotation values", default = False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
fcurves: bpy.props.IntProperty(name='fcurves', description='helper to check if fcurves are changed', default=0, override = {'LIBRARY_OVERRIDABLE'})
upper_stack : bpy.props.BoolProperty(name="Upper Stack Evaluation", description="Checks if tweak mode uses upper stack", default=False, override = {'LIBRARY_OVERRIDABLE'})
viewlayer : bpy.props.BoolProperty(name="View Layer Exclusion", description="Check if the object was added or removed from the view layer", default=True, override = {'LIBRARY_OVERRIDABLE'})
#tools
#Tools
inbetweener : bpy.props.FloatProperty(name='Inbetween Keyframe', description="Adds an inbetween Keyframe between the Layer's neighbor keyframes", soft_min = 0, soft_max = 1, default=0.5, options = set(), override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.add_inbetween_key)
share_layer_keys: bpy.props.EnumProperty(name = 'Share Layer Keys', description="Share keyframes positions between layers", items = anim_layers.share_layerkeys_items, override = {'LIBRARY_OVERRIDABLE'})
influence_hide: bpy.props.BoolProperty(name="Hide Influence", description="Hide Influence Fcurves", default=False, update = anim_layers.influence_hide_keyframes, override = {'LIBRARY_OVERRIDABLE'})
influence_lock: bpy.props.BoolProperty(name="Lock Influence", description="Lock Influence Fcurves", default=False, update = anim_layers.influence_lock_keyframes, override = {'LIBRARY_OVERRIDABLE'})
influence_mute: bpy.props.BoolProperty(name="Mute Influence", description="Mute Influence Fcurves", default=False, update = anim_layers.influence_mute_fcurves, override = {'LIBRARY_OVERRIDABLE'})
class AnimLayersItems(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="AnimLayer", override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_name_update)
mute: bpy.props.BoolProperty(name="Mute", description="Mute Animation Layer", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_mute)
lock: bpy.props.BoolProperty(name="Lock", description="Lock Animation Layer", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_lock)
solo: bpy.props.BoolProperty(name="Solo", description="Solo Animation Layer", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_solo)
influence: bpy.props.FloatProperty(name="Layer Influence", description="Layer Influence", min = 0.0, options={'ANIMATABLE'}, max = 1.0, default = 1.0, precision = 3, update = anim_layers.influence_update, override = {'LIBRARY_OVERRIDABLE'})
influence_mute: bpy.props.BoolProperty(name="Animated Influence", description="Turn Animated influence On/Off", default=False, options={'HIDDEN'}, update = anim_layers.influence_mute_update, override = {'LIBRARY_OVERRIDABLE'})
#action_list: bpy.props.EnumProperty(name = 'Actions', description = "Select action", update = anim_layers.load_action, items = anim_layers.action_items, override = {'LIBRARY_OVERRIDABLE'})
influence: bpy.props.FloatProperty(name="Layer Influence", description="Layer Influence", min = 0.0, options={'ANIMATABLE'}, max = 1.0, default = 1.0, precision = 3, update = anim_layers.influence_update) #
action: bpy.props.PointerProperty(name = 'action', description = "Select action", type=bpy.types.Action, update = anim_layers.load_action, override = {'LIBRARY_OVERRIDABLE'})
action_range: bpy.props.FloatVectorProperty(name='action range', description="used to check if layer needs to update frame range", override = {'LIBRARY_OVERRIDABLE'}, size = 2)
frame_range: bpy.props.BoolProperty(name="Custom Frame Range", description="Use a custom frame range per layer instead of the scene frame range", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_frame_range)
custom_frame_range: bpy.props.BoolProperty(name="Custom Frame Range", description="Use a custom frame range per layer instead of the scene frame range", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_frame_range)
frame_start: bpy.props.FloatProperty(name='Action Start Frame', description="First frame of the layer's action",min = 0, default=0, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_frame_start)
frame_end: bpy.props.FloatProperty(name='Action End Frame', description="End frame of the layer's action", default=0, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_frame_end)
speed: bpy.props.FloatProperty(name='Speed of the action', description="Speed of the action strip", default = 1, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_speed)
@@ -129,12 +137,15 @@ class AnimLayersObjects(bpy.types.PropertyGroup):
object: bpy.props.PointerProperty(name = "object", description = "objects with animation layers turned on", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
# Add-ons Preferences Update Panel
# Define Panel classes for updating
panels = (anim_layers.ANIMLAYERS_PT_List, anim_layers.ANIMLAYERS_PT_Ops, anim_layers.ANIMLAYERS_PT_Tools, anim_layers.ANIMLAYERS_PT_Settings) #anim_layers.ANIMLAYERS_PT_Panel, anim_layers.ANIMLAYERS_PT_Multikey,
panels = anim_layers.panel_classes
def update_panel(self, context):
anim_layers.unregister_panels()
anim_layers.register_panels()
message = "AnimationLayers: Updating Panel locations has failed"
try:
for panel in panels:
@@ -165,6 +176,8 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
items = [('ANIMLAYERS', 'Anim Layers Settings', 'Use Anim Layers properties to adjust custom frame range'),
('NLA', 'NLA Settings', 'Use the nla properties to adjust custom frame range')])
lock_nlatracks: bpy.props.BoolProperty(name="Automatically lock the nla tracks for safety measures", description="Automatically lock nla tracks when creating layers for safety", default = True)
#Property for ClearActiveAction
proceed: bpy.props.EnumProperty(name="Choose how to proceed", description="Select an option how to proceed with Anim Layers", override = {'LIBRARY_OVERRIDABLE'},
items = [
@@ -172,6 +185,15 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
( 'REMOVE_ACTION', 'Remove current action and reload older Layers', 'Remove current action and continue with the previous layers', 1),
('ADD_ACTION', 'Add the current action as a new Layer', 'Keep previous Anim Layers and Add the active action as a new layer', 2),])
enabled_editors: bpy.props.EnumProperty(
name="Enabled Editors",
description="Select which editors should show animation layers panels",
items=[('VIEW_3D', "3D View", ""), ('GRAPH_EDITOR', "Graph Editor", ""), ('DOPESHEET_EDITOR', "Dope Sheet", ""),('NLA_EDITOR', "NLA Editor", ""),],
options={'ENUM_FLAG'},
default={'VIEW_3D', 'NLA_EDITOR', 'DOPESHEET_EDITOR', 'GRAPH_EDITOR'},
update=update_panel
)
category: bpy.props.StringProperty(
name="Tab Category",
description="Choose a name for the category of the panel",
@@ -183,7 +205,7 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
auto_check_update: bpy.props.BoolProperty(
name = "Auto-check for Update",
description = "If enabled, auto-check for updates using an interval",
default = False,
default = True,
)
updater_interval_months: bpy.props.IntProperty(
@@ -222,14 +244,14 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
col.label(text="Tab Category:")
col.prop(self, "category", text="")
row = layout.row()
row.prop(self, "enabled_editors")
col = layout.column()
col.separator(factor = 1)
col = col.box()
col.label(text="Defaults:")
#row = layout.row()
split = col.split(factor=0.5, align = True)
split.prop(self, "auto_rename")
#row.prop(self, "blend_type")
#split = row.split(factor=0.7, align = True)
split.label(text="Default Blend Type: ")
split.prop(self,'blend_type', text = '')
@@ -237,11 +259,12 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
row = col.row()
row.label(text = "Custom Frame Range Settings")
row.prop(self, "frame_range_settings", text = '')
#row = layout.row(align = True)
#col = layout.row(bpy.context.scene.als,'blend_type', text = '')
col.prop(self, "lock_nlatracks")
classes = (AnimLayersSettings, AnimLayersSceneSettings, AnimLayersItems, AnimLayersObjects)
addon_keymaps = []
def register():
@@ -1374,9 +1374,9 @@ def register(bl_info):
# **WARNING** Depending on the engine, this token can act like a password!!
# Only provide a token if the project is *non-public*, see readme for
# other considerations and suggestions from a security standpoint
# updater.private_token = "kgsE5RsxTzDvQRs9P_Yg" # "tokenstring"
updater.private_token = "glpat-K6GjFFka_J6o3GTbe3JK"
# "tokenstring" Need read api and read repo permission
# updater.private_token = "glpat-K6GjFFka_J6o3GTbe3JK"
updater.private_token = "glpat-js__ikigVSQ_tKWgjVn1"
# choose your own username, must match website (not needed for GitLab)
updater.user = ""
@@ -1508,7 +1508,7 @@ def register(bl_info):
# max install (<) will install strictly anything lower
# updater.version_max_update = (9,9,9)
updater.version_max_update = (2,1,8,7) # set to None if not wanting to set max
updater.version_max_update = None # set to None if not wanting to set max
# Function defined above, customize as appropriate per repository
updater.skip_tag = skip_tag_function # min and max used in this function
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
{
"last_check": "2025-05-28 13:56:15.908215",
"last_check": "2025-11-26 10:56:41.294043",
"backup_date": "",
"update_ready": false,
"ignore": false,
@@ -1,311 +0,0 @@
# ***** 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": "Animation Layers",
"author": "Tal Hershkovich",
"version" : (2, 1, 9, 7),
"blender" : (3, 2, 0),
"location": "View3D - Properties - Animation Panel",
"description": "Simplifying the NLA editor into an animation layers UI and workflow",
"wiki_url": "",
"category": "Animation"}
if "bpy" in locals():
import importlib
if "bake_ops" in locals():
importlib.reload(bake_ops)
if "anim_layers" in locals():
print('reloading anim layers')
importlib.reload(anim_layers)
if "subscriptions" in locals():
importlib.reload(subscriptions)
if "multikey" in locals():
importlib.reload(multikey)
if "addon_updater_ops" in locals():
importlib.reload(addon_updater_ops)
import bpy
from . import addon_updater_ops
from . import anim_layers
from . import bake_ops
from . import subscriptions
from . import multikey
from bpy.utils import register_class
from bpy.utils import unregister_class
class AnimLayersSceneSettings(bpy.types.PropertyGroup):
bake_range_type: bpy.props.EnumProperty(name = 'Bake Range', description="Use either scene, actions length or custom frame range", default = 'SCENE', update = bake_ops.bake_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)
handles_type: bpy.props.EnumProperty(name="Handle Types", description="Select handle type before recalculating the handle values", override = {'LIBRARY_OVERRIDABLE'}, default = 'FREE',
items = [
('PRESERVE', 'Preserve', 'Preserves previous Bezier handlers types before recalculation', 0),
( 'FREE', 'Free', 'Bezier handlers get Free handle types before recalculation', 1),
('ALIGNED', 'Aligned', 'Bezier handlers get Aligned handle types before recalculation', 2),
('VECTOR','Vector', 'Bezier handlers get Vector handle types', 3),
('AUTO', 'Auto', 'Bezier handlers get Auto handle types before recalculation',4),
('AUTO_CLAMPED', 'Auto Clamped', 'Bezier handlers get Free handle types before recalculation', 5)
]
)
class AnimLayersSettings(bpy.types.PropertyGroup):
turn_on: bpy.props.BoolProperty(name="Turn Animation Layers On", description="Turn on and start Animation Layers", default=False, options={'HIDDEN'}, update = anim_layers.turn_animlayers_on, override = {'LIBRARY_OVERRIDABLE'})
layer_index: bpy.props.IntProperty(update = anim_layers.update_layer_index, options={'LIBRARY_EDITABLE'}, default = 0, override = {'LIBRARY_OVERRIDABLE'})
linked: bpy.props.BoolProperty(name="Linked", description="Duplicate a layer with a linked action", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
#Bake settings
smartbake: bpy.props.BoolProperty(name="Smart Bake", description="Stay with the same amount of keyframes after merging and baking", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
onlyselected: bpy.props.BoolProperty(name="Only selected Bones", description="Bake only selected Armature controls", default=True, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
clearconstraints: bpy.props.BoolProperty(name="Clear constraints", description="Clear constraints during bake", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
mergefcurves: bpy.props.BoolProperty(name="Merge Cyclic & Fcurve modifiers", description="Include Fcurve modifiers in the bake", default = True, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
view_all_keyframes: bpy.props.BoolProperty(name="View", description="View keyframes from multiple layers, use lock and mute to exclude layers", default=False, update = anim_layers.view_all_keyframes, override = {'LIBRARY_OVERRIDABLE'})
edit_all_keyframes: bpy.props.BoolProperty(name="Edit", description="Edit keyframes from multiple layers", default=False, update = anim_layers.unlock_edit_keyframes, override = {'LIBRARY_OVERRIDABLE'})
only_selected_bones: bpy.props.BoolProperty(name="Only Selected Bones", description="Edit and view only selected bones", default=True, update = anim_layers.only_selected_bones, override = {'LIBRARY_OVERRIDABLE'})
view_all_type: bpy.props.EnumProperty(name="Type", description="Select visibiltiy type of keyframes", update = anim_layers.view_all_keyframes, override = {'LIBRARY_OVERRIDABLE'}, default = '2',
items = [
('0', 'Breakdown', 'select Breakdown visibility'),
('1', 'Jitter', 'select Jitter visibility'),
('2', 'Moving Hold', 'select Moving Hold visibility'),
('3', 'Extreme', 'select Extreme visibility'),
('4', 'Keyframe', 'select Keyframe visibility')
]
)
baketype : bpy.props.EnumProperty(name = '', description="Type of Bake", items = [('AL', 'Anim Layers','Use Animation Layers Bake',0), ('NLA', 'NLA Bake', 'Use Blender internal NLA Bake',1)], override = {'LIBRARY_OVERRIDABLE'})
direction: bpy.props.EnumProperty(name = '', description="Select direction of merge", items = [('UP', 'Up','Merge upwards','TRIA_UP',1), ('DOWN', 'Down', 'Merge downwards','TRIA_DOWN',0), ('ALL', 'All', 'Merge all layers')], override = {'LIBRARY_OVERRIDABLE'})
operator : bpy.props.EnumProperty(name = '', description="Type of bake", items = [('NEW', 'New Baked Layer','Bake into a New Layer','NLA',1), ('MERGE', 'Merge', 'Merge Layers','NLA_PUSHDOWN',0)], override = {'LIBRARY_OVERRIDABLE'})
blend_type : bpy.props.EnumProperty(name = 'Blend Type', description="Blend Type",
items = [('REPLACE', 'Replace', 'Use as a Base Layer'), ('ADD', 'Add', 'Use as an Additive Layer'), ('SUBTRACT', 'Subtract', 'Use as an Subtract Layer')], update = anim_layers.blend_type_update , override = {'LIBRARY_OVERRIDABLE'})
data_type : bpy.props.EnumProperty(name = 'Data Type', description="Select type of action data", default = 'OBJECT', update = anim_layers.data_type_update,
items = [('KEY', 'Shapekey', 'Switch to shapekey animation layers'), ('OBJECT', 'Object', 'Switch to object animation')], override = {'LIBRARY_OVERRIDABLE'})#, update = anim_layers.blend_type_update
auto_rename: bpy.props.BoolProperty(name="Auto Rename Layer", description="Rename layer to match to selected action", default = True, update = anim_layers.auto_rename, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
auto_blend: bpy.props.BoolProperty(name="Auto Blend", description="Apply blend type automatically based on scale and rotation values", default = False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
fcurves: bpy.props.IntProperty(name='fcurves', description='helper to check if fcurves are changed', default=0, override = {'LIBRARY_OVERRIDABLE'})
upper_stack : bpy.props.BoolProperty(name="Upper Stack Evaluation", description="Checks if tweak mode uses upper stack", default=False, override = {'LIBRARY_OVERRIDABLE'})
#Tools
inbetweener : bpy.props.FloatProperty(name='Inbetween Keyframe', description="Adds an inbetween Keyframe between the Layer's neighbor keyframes", soft_min = 0, soft_max = 1, default=0.5, options = set(), override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.add_inbetween_key)
share_layer_keys: bpy.props.EnumProperty(name = 'Share Layer Keys', description="Share keyframes positions between layers", items = anim_layers.share_layerkeys_items, override = {'LIBRARY_OVERRIDABLE'})
class AnimLayersItems(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="AnimLayer", override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_name_update)
mute: bpy.props.BoolProperty(name="Mute", description="Mute Animation Layer", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_mute)
lock: bpy.props.BoolProperty(name="Lock", description="Lock Animation Layer", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_lock)
solo: bpy.props.BoolProperty(name="Solo", description="Solo Animation Layer", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_solo)
influence: bpy.props.FloatProperty(name="Layer Influence", description="Layer Influence", min = 0.0, options={'ANIMATABLE'}, max = 1.0, default = 1.0, precision = 3, update = anim_layers.influence_update, override = {'LIBRARY_OVERRIDABLE'})
influence_mute: bpy.props.BoolProperty(name="Animated Influence", description="Turn Animated influence On/Off", default=False, options={'HIDDEN'}, update = anim_layers.influence_mute_update, override = {'LIBRARY_OVERRIDABLE'})
#action_list: bpy.props.EnumProperty(name = 'Actions', description = "Select action", update = anim_layers.load_action, items = anim_layers.action_items, override = {'LIBRARY_OVERRIDABLE'})
action: bpy.props.PointerProperty(name = 'action', description = "Select action", type=bpy.types.Action, update = anim_layers.load_action, override = {'LIBRARY_OVERRIDABLE'})
action_range: bpy.props.FloatVectorProperty(name='action range', description="used to check if layer needs to update frame range", override = {'LIBRARY_OVERRIDABLE'}, size = 2)
frame_range: bpy.props.BoolProperty(name="Custom Frame Range", description="Use a custom frame range per layer instead of the scene frame range", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_frame_range)
frame_start: bpy.props.FloatProperty(name='Action Start Frame', description="First frame of the layer's action",min = 0, default=0, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_frame_start)
frame_end: bpy.props.FloatProperty(name='Action End Frame', description="End frame of the layer's action", default=0, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_frame_end)
speed: bpy.props.FloatProperty(name='Speed of the action', description="Speed of the action strip", default = 1, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_speed)
offset: bpy.props.FloatProperty(name='Offset when the action starts', description="Offseting the whole layer animation", default = 0, precision = 2, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_offset)
repeat: bpy.props.FloatProperty(name="Repeat", description="Repeat the action", min = 0.1, default = 1, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_repeat)
class AnimLayersObjects(bpy.types.PropertyGroup):
object: bpy.props.PointerProperty(name = "object", description = "objects with animation layers turned on", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
# Add-ons Preferences Update Panel
# Define Panel classes for updating
panels = anim_layers.panel_classes
def update_panel(self, context):
anim_layers.unregister_panels()
anim_layers.register_panels()
message = "AnimationLayers: Updating Panel locations has failed"
try:
for panel in panels:
if "bl_rna" in panel.__dict__:
bpy.utils.unregister_class(panel)
for panel in panels:
#print (panel.bl_category)
panel.bl_category = context.preferences.addons[__name__].preferences.category
bpy.utils.register_class(panel)
except Exception as e:
print("\n[{}]\n{}\n\nError:\n{}".format(__name__, message, e))
pass
@addon_updater_ops.make_annotations
class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
# this must match the addon name, use '__package__'
# when defining this in a submodule of a python package.
bl_idname = __package__
auto_rename: bpy.props.BoolProperty(name="Sync Layer/Action Names", description="Rename layer to match to selected action", default = True)
blend_type : bpy.props.EnumProperty(name = 'Default Blend Type', description="Default Blend Type when adding layers", default = 'COMBINE',
items = [('COMBINE', 'Combine', 'Use Combine as the default blend type'), ('ADD', 'Add', 'Use Add as the default blend type'), ('REPLACE', 'Replace', 'Use Replace as the default blend type'),
('SUBTRACT', 'Subtract', 'Use as an Subtract Layer')])
frame_range_settings : bpy.props.EnumProperty(name = 'Frame Range Settings', description="Use Either Anim Layers Custom Frame Range Settings or NLA Settings", default = 'ANIMLAYERS',
items = [('ANIMLAYERS', 'Anim Layers Settings', 'Use Anim Layers properties to adjust custom frame range'),
('NLA', 'NLA Settings', 'Use the nla properties to adjust custom frame range')])
#Property for ClearActiveAction
proceed: bpy.props.EnumProperty(name="Choose how to proceed", description="Select an option how to proceed with Anim Layers", override = {'LIBRARY_OVERRIDABLE'},
items = [
('REMOVE_LAYERS', 'Remove old layers and continue with the current action', 'Remove previous Layers and continue with current action in the base layer', 0),
( 'REMOVE_ACTION', 'Remove current action and reload older Layers', 'Remove current action and continue with the previous layers', 1),
('ADD_ACTION', 'Add the current action as a new Layer', 'Keep previous Anim Layers and Add the active action as a new layer', 2),])
enabled_editors: bpy.props.EnumProperty(
name="Enabled Editors",
description="Select which editors should show animation layers panels",
items=[('VIEW_3D', "3D View", ""), ('GRAPH_EDITOR', "Graph Editor", ""), ('DOPESHEET_EDITOR', "Dope Sheet", ""),('NLA_EDITOR', "NLA Editor", ""),],
options={'ENUM_FLAG'},
default={'VIEW_3D', 'NLA_EDITOR'},
update=update_panel
)
category: bpy.props.StringProperty(
name="Tab Category",
description="Choose a name for the category of the panel",
default="Animation",
update=update_panel
)
# 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 = False,
)
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
)
def draw(self, context):
layout = self.layout
addon_updater_ops.update_settings_ui(self, context)
col = layout.column()
col.label(text="Tab Category:")
col.prop(self, "category", text="")
row = layout.row()
row.prop(self, "enabled_editors")
col = layout.column()
col.separator(factor = 1)
col = col.box()
col.label(text="Defaults:")
#row = layout.row()
split = col.split(factor=0.5, align = True)
split.prop(self, "auto_rename")
#row.prop(self, "blend_type")
#split = row.split(factor=0.7, align = True)
split.label(text="Default Blend Type: ")
split.prop(self,'blend_type', text = '')
row = col.row()
row.label(text = "Custom Frame Range Settings")
row.prop(self, "frame_range_settings", text = '')
#row = layout.row(align = True)
#col = layout.row(bpy.context.scene.als,'blend_type', text = '')
classes = (AnimLayersSettings, AnimLayersSceneSettings, AnimLayersItems, AnimLayersObjects)
addon_keymaps = []
def register():
addon_updater_ops.register(bl_info)
register_class(AnimLayersAddonPreferences)
addon_updater_ops.make_annotations(AnimLayersAddonPreferences) # to avoid blender 2.8 warnings
if bpy.app.version < (3, 2, 0):
return
bake_ops.register()
anim_layers.register()
multikey.register()
for cls in classes:
register_class(cls)
bpy.types.Object.als = bpy.props.PointerProperty(type = AnimLayersSettings, options={'LIBRARY_EDITABLE'}, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.Scene.als = bpy.props.PointerProperty(type = AnimLayersSceneSettings, options={'LIBRARY_EDITABLE'}, override = {'LIBRARY_OVERRIDABLE'})
bpy.types.Object.Anim_Layers = bpy.props.CollectionProperty(type = AnimLayersItems, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
bpy.types.Scene.AL_objects = bpy.props.CollectionProperty(type = AnimLayersObjects, options={'LIBRARY_EDITABLE'}, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
update_panel(None, bpy.context)
#update_tweak_keymap()
#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= 'NLA Generic', space_type= 'NLA_EDITOR')
if 'nla.tweakmode_enter' not in km.keymap_items:
kmi = km.keymap_items.new('nla.tweakmode_enter', type= 'TAB', value= 'PRESS')
kmi.properties.use_upper_stack_evaluation = True
addon_keymaps.append((km, kmi))
def unregister():
addon_updater_ops.unregister()
unregister_class(AnimLayersAddonPreferences)
if bpy.app.version < (3, 2, 0):
return
for cls in classes:
unregister_class(cls)
bake_ops.unregister()
anim_layers.unregister()
multikey.unregister()
del bpy.types.Object.als
del bpy.types.Object.Anim_Layers
del bpy.types.Scene.AL_objects
#removing keymaps
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
@@ -1,17 +0,0 @@
{
"last_check": "2025-06-04 16:35:52.082677",
"backup_date": "May-6-2025",
"update_ready": true,
"ignore": false,
"just_restored": false,
"just_updated": false,
"version_text": {
"link": "https://gitlab.com/api/v4/projects/22294607/repository/archive.zip?sha=d8a2bbe2878233bf807b592e59fd534918d2dabf",
"version": [
2,
1,
9,
8
]
}
}
File diff suppressed because it is too large Load Diff
@@ -1,510 +0,0 @@
# if "bpy" in locals():
# import importlib
# if "multikey" in locals():
# importlib.reload()
import bpy
import random
import numpy as np
from mathutils import Quaternion
from . import bake_ops
from . import anim_layers
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]
source = obj.pose.bones[bone]
#in case of shapekey animation
elif fcu_key[0][:10] == 'key_blocks':
attr = fcu_key[0].split('"')[1]
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[fcu_key[1]]
return attrvalue
#in case of property on object
elif 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:
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 filter_properties(obj, fcu):
'Filter the W X Y Z attributes of the transform properties'
transformations = ["rotation_quaternion","rotation_euler", "location", "scale"]
#check if the fcurve data path ends with any of the transformations
if not any(fcu.data_path.endswith(transform) for transform in transformations):
return True
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
#in case of channels like bbone_scalein that are no included then return
if not hasattr(bpy.context.scene.multikey, transform):
return True
attr = getattr(bpy.context.scene.multikey, transform)
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(obj, fcurves, path, current_value, eval_array):
'''Get the difference value and add it to all selected keyframes'''
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 not filter_properties(obj, 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 = "anim.multikey_scale_value"
bl_label = "Scale Values"
bl_options = {'REGISTER', 'UNDO'}
def invoke(self, context, event):
#reset the values for dragging
self.stop = False
scene = context.scene
global is_dragging
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
action = obj.animation_data.action
fcurves = anim_layers.get_fcurves_channelbag(obj, action)
for fcu in fcurves:
if anim_layers.selected_bones_filter(obj, fcu):
continue
if not filter_properties(obj, 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:
if 'is_dragging' in globals():
del is_dragging
scene.multikey['scale'] = 1
anim_layers.redraw_areas(['VIEW_3D'])
return('CANCELLED')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
global is_dragging
try:
scene = context.scene
scale = scene.multikey.scale
#Quit the modal operator when the slider is released
if self.stop:
del is_dragging
scene.multikey['scale'] = 1
anim_layers.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'}
except Exception as e:
# Log the error
print("Error:", e)
self['scale'] = 1
self.stop = True
del is_dragging
return {'CANCELLED'}
def scale_value(self, context):
if 'is_dragging' in globals():
if 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.anim.multikey_scale_value('INVOKE_DEFAULT')
def random_value(self, context):
for obj in context.selected_objects:
if obj.animation_data.action is None:
continue
action = obj.animation_data.action
fcurves = anim_layers.get_fcurves_channelbag(obj, action)
for fcu in fcurves:
# if obj.mode == 'POSE':
if anim_layers.selected_bones_filter(obj, fcu):
continue
if not filter_properties(obj, fcu):
continue
value_list = []
threshold = bpy.context.scene.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()
self['randomness'] = 0.1
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 = array_default if array_len == 4 else [0, 0, 0]
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 = anim_layers.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 = anim_layers.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:# or len(fcu_array) != len(eval_array):
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):
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 obj.als.onlyselected else obj.pose.bones
fcurves = anim_layers.get_fcurves_channelbag(obj, action)
for fcu in fcurves:
if fcu in fcu_paths:
continue
if obj.mode == 'POSE':
if anim_layers.selected_bones_filter(obj, fcu):
continue
if not filter_properties(obj, fcu):
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(bake_ops.attr_default(obj, (fcu.data_path, fcu.array_index)))
eval_array = evaluate_layers(context, obj, anim_data, fcu, array_default)
if eval_array is None:
fcurves = anim_layers.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(obj, 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 = "fcurves.multikey"
bl_options = {'REGISTER', 'UNDO'}
# bl_description = ('Select keyframes, move your bone or objecet and press the operator. Does not work with Autokey')
@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="Affect only 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 Values Factor", description="Scale percentage from the average value", default=1.0, soft_max = 10, soft_min = -10, step=0.1, precision = 3, 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)
# is_dragging: bpy.props.BoolProperty(default = False)
#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 = (MultikeyProperties, FilterProperties, MULTIKEY_OT_Multikey, ScaleValuesOp)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
bpy.types.Scene.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.multikey
@@ -1,649 +0,0 @@
import bpy
from . import anim_layers
from . import bake_ops
def subscriptions_remove(handler = True):
#clear all handlers and subsciptions
bpy.msgbus.clear_by_owner(bpy.context.scene)
global influence_keys, selected_bones
if 'influence_keys' in globals():
del influence_keys
if 'selected_bones' in globals():
del selected_bones
if not handler:
return
if check_handler in bpy.app.handlers.depsgraph_update_pre:
bpy.app.handlers.depsgraph_update_pre.remove(check_handler)
if animlayers_frame in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.remove(animlayers_frame)
def subscriptions_add(scene, handler = True):
bpy.msgbus.clear_by_owner(scene)
subscribe_to_frame_end(scene)
subscribe_to_track_name(scene)
subscribe_to_action_name(scene)
subscribe_to_strip_settings(scene)
subscribe_to_influence(scene)
if not handler:
return
if check_handler not in bpy.app.handlers.depsgraph_update_pre:
bpy.app.handlers.depsgraph_update_pre.append(check_handler)
if animlayers_frame not in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.append(animlayers_frame)
def animlayers_frame(self, context):
scene = bpy.context.scene
current = scene.frame_current_final
#Make sure the animation is playing and not just running a motion path
if not bpy.context.screen.is_animation_playing:
return
#Checking if preview range was turned on or off, when using hotkey P it doesn't recognize
#only during the frame handler
if scene.get('framerange_preview') != scene.use_preview_range:
scene['framerange_preview'] = scene.use_preview_range
frameend_update_callback()
return
frame_start, frame_end = bake_ops.frame_start_end(scene)
reset_subscription = False
if 'outofrange' not in globals():
global outofrange
outofrange = False if 0 <= current <= frame_end else True
# print('out of range ', outofrange )
if 0 <= current <= frame_end:
if outofrange:
frameend_update_callback()
outofrange = False
return
outofrange = True
# if current <= frame_end:
# return
#iterate only through objects with anim layers turned on
objects = [obj.object for obj in scene.AL_objects]
for obj in objects:
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
return
nla_tracks = anim_data.nla_tracks
if not len(nla_tracks):
return
for i, track in enumerate(nla_tracks):
if len(track.strips) != 1:
continue
#checks if the layer has a custom frame range
layer = obj.Anim_Layers[i]
if layer.frame_range:
continue
if not reset_subscription:
subscriptions_remove(handler = False)
reset_subscription = True
strip = track.strips[0]
if current < 0:
# anim_layers.strip_action_recalc(layer, track.strips[0])
strip.frame_start_ui = current
anim_layers.update_action_frame_range(current, frame_end, layer, strip)
# track.strips[0].action_frame_start = current * 1/layer.speed - layer.offset * 1/layer.speed
strip.frame_end_ui = frame_end
elif current > frame_end:
if strip.frame_start < 0:
strip.frame_start_ui = 0
anim_layers.update_action_frame_range(0, frame_end, layer, strip)
# print('animlayers_frame ', current)
strip.frame_end_ui = current + 10
if reset_subscription:
subscriptions_add(scene, handler = False)
def check_handler(self, context):
'''A main function that performs a series of checks using a handler'''
scene = bpy.context.scene
#if there are no objects included in animation layers then return
if not len(scene.AL_objects):
return
obj = bpy.context.object
#if the object was removed from the scene, then remove it from anim layers object list
if obj is None:
i = 0
while i < len(scene.AL_objects):
if scene.AL_objects[i].object not in scene.objects.values():
scene.AL_objects.remove(i)
else:
i += 1
return
if not obj.als.turn_on:
return
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
return
if not anim_data.use_nla:
obj.als.turn_on = False
return
if not len(obj.Anim_Layers):
return
if not hasattr(anim_data, 'nla_tracks') or not obj.als.turn_on: #obj.select_get() == False or
return
anim_layers.add_obj_to_animlayers(obj, [item.object for item in scene.AL_objects])
nla_tracks = anim_data.nla_tracks
layer = obj.Anim_Layers[obj.als.layer_index]
active_action_update(obj, anim_data, nla_tracks)
#check if a keyframe was removed
if bpy.context.active_operator is not None:
if bpy.context.active_operator.name in ['Transform', 'Delete Keyframes'] and obj.als.edit_all_keyframes:
anim_layers.edit_all_keyframes()
if bpy.context.active_operator.name == 'Enter Tweak Mode':
if not bpy.context.active_operator.properties['use_upper_stack_evaluation']:
obj.als.upper_stack = False
if bpy.context.active_operator.name == 'Move Channels':
anim_layers.visible_layers(obj, nla_tracks)
# check if track and layers are synchronized
if track_layer_synchronization(obj, nla_tracks):
return
anim_layers.add_obj_to_animlayers(obj, [item.object for item in scene.AL_objects])
track = nla_tracks[obj.als.layer_index]
always_sync_range(track, layer)
# sync_strip_range(track, layer)
if anim_data.use_tweak_mode and layer.lock:
layer['lock'] = False
elif not anim_data.use_tweak_mode and not layer.lock:
layer['lock'] = True
influence_sync(obj, nla_tracks)
# continue if locked
if layer.lock:
return
if obj.als.view_all_keyframes:
anim_layers.hide_view_all_keyframes(obj, anim_data)
check_selected_bones(obj)
influence_check(nla_tracks[obj.als.layer_index])
def track_layer_synchronization(obj, nla_tracks):
'''check if track and layers are synchronized'''
if len(nla_tracks) == len(obj.Anim_Layers):
return False
new_layers_names = set(track.name for track in nla_tracks).difference(set(layer.name for layer in obj.Anim_Layers))
anim_layers.visible_layers(obj, nla_tracks)
if obj.als.layer_index > len(obj.Anim_Layers)-1:
obj.als.layer_index = len(obj.Anim_Layers)-1
#update new layer with strip settings
frame_start, frame_end = bake_ops.frame_start_end(bpy.context.scene)
for layer_name in new_layers_names:
if len(nla_tracks[layer_name].strips) != 1:
continue
strip = get_strip_in_meta(nla_tracks[layer_name].strips[0])
layer = obj.Anim_Layers[layer_name]
if (strip.frame_start, strip.frame_end) != (frame_start, frame_end):
layer['frame_range'] = True
update_strip_layer_settings(strip, layer)
layer['action'] = strip.action
return True
def active_action_update(obj, anim_data, nla_tracks):
'''updating the active action into the selected layer'''
if obj.Anim_Layers[obj.als.layer_index].lock:
if anim_data.action != None:
subscriptions_remove()
anim_data.use_tweak_mode = False
anim_data.action = None
subscriptions_add(bpy.context.scene)
return
if anim_data.action == nla_tracks[obj.als.layer_index].strips[0].action:
return
if not len(nla_tracks[obj.als.layer_index].strips):
return
if not anim_data.action or anim_data.is_property_readonly('action'):
return
subscriptions_remove()
action = anim_data.action
anim_data.action = None
obj.Anim_Layers[obj.als.layer_index].action = action
subscriptions_add(bpy.context.scene)
def get_strip_in_meta(strip):
'''check if it's meta strip then access the last strip inside meta hierarchy'''
while len(strip.strips):
strip = strip.strips[0]
return strip
def sync_strip_range():
scene = bpy.context.scene
frame_start, frame_end = bake_ops.frame_start_end(scene)
if scene.frame_current_final > frame_end:
frame_end = scene.frame_current_final + 10
frame_start = scene.frame_current_final if scene.frame_current_final < 0 else 0.0
objects = [obj.object for obj in scene.AL_objects]
for obj in objects:
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
continue
nla_tracks = anim_data.nla_tracks
if not len(nla_tracks):
continue
for i, track in enumerate(nla_tracks):
if len(track.strips) != 1:
continue
if obj.Anim_Layers[i]['frame_range']:
continue
strip = track.strips[0]
strip_frame_start = strip.frame_start
strip_frame_end = strip.frame_end
if (strip_frame_start, round(strip_frame_end, 2)) != (frame_start, float(frame_end)):
obj.Anim_Layers[i]['frame_range'] = True
def always_sync_range(track, layer):
'''sync frame range when always sync turned on'''
if not len(track.strips):
return
if not layer.frame_range:
if track.strips[0].use_sync_length:
track.strips[0].use_sync_length = False
return
if not track.strips[0].use_sync_length:
if tuple(layer.action_range) != (0.0, 0.0): #reset action range when turned off
layer.action_range = (0.0, 0.0)
return
strip = track.strips[0]
if tuple(layer.action_range) == tuple((strip.action.frame_range[0], strip.action.frame_range[1])):
return
anim_layers.sync_frame_range(bpy.context)
layer.action_range = strip.action.frame_range
def influence_sync(obj, nla_tracks):
#Tracks that dont have keyframes are locked
for i, track in enumerate(nla_tracks):
if obj.Anim_Layers[i].lock:
continue
if not len(track.strips):
continue
if not len(track.strips[0].fcurves):
continue
if not len(track.strips[0].fcurves[0].keyframe_points):
#apply the influence property to the temp property when keyframes are removed (but its still locked)
if not track.strips[0].fcurves[0].lock:
obj.Anim_Layers[i].influence = track.strips[0].influence
track.strips[0].fcurves[0].lock = True
if obj.animation_data is None:
return
action = obj.animation_data.action
if action is None:
return
#if a keyframe was found in the temporary property then add it to the
data_path = 'Anim_Layers[' + str(obj.als.layer_index) + '].influence'
fcurves = anim_layers.get_fcurves_channelbag(obj, action)
fcu_influence = fcurves.find(data_path)
if fcu_influence is None:
return
if not len(fcu_influence.keyframe_points):
return
#remove the temporary influence
fcurves.remove(fcu_influence)
#if the action was created just for the influence because of empty object data type then remove the action
if action.name == obj.name + 'Action' and not len(obj.animation_data.nla_tracks) and not len(fcurves):
bpy.data.actions.remove(action)
if obj.Anim_Layers[obj.als.layer_index].influence_mute:
return
strip = nla_tracks[obj.als.layer_index].strips[0]
strip.fcurves[0].lock = False
strip.keyframe_insert('influence')
strip.fcurves[0].update()
def influence_check(selected_track):
'''update influence when a keyframe was added without autokey'''
#skip the next steps if a strip is missing or tracks were removed from the nla tracks
if len(selected_track.strips) != 1:# or obj.als.layer_index > len(nla_tracks)-2:
return
if not len(selected_track.strips[0].fcurves):
return
global influence_keys
if selected_track.strips[0].fcurves[0].mute or not len(selected_track.strips[0].fcurves[0].keyframe_points) or bpy.context.scene.tool_settings.use_keyframe_insert_auto:
if 'influence_keys' in globals():
del influence_keys
return #when the fcurve doesnt have keyframes, or when autokey is turned on, then return
#update if the influence keyframes are changed. influence_keys are first added in influence_update_callback
if 'influence_keys' not in globals():
return
if influence_keys != [tuple(key.co) for key in selected_track.strips[0].fcurves[0].keyframe_points]:
selected_track.strips[0].fcurves[0].update()
del influence_keys
def check_selected_bones(obj):
'''running in the handler and checking if the selected bones were changed during view multiply layer keyframes'''
if not obj.als.only_selected_bones:
return
global selected_bones
try:
selected_bones
except NameError:
selected_bones = bpy.context.selected_pose_bones
return
else:
if selected_bones != bpy.context.selected_pose_bones:
selected_bones = bpy.context.selected_pose_bones
obj.als.view_all_keyframes = True
########################### MSGBUS SUBSCRIPTIONS #############################
#Callback function for Scene frame end
def frameend_update_callback():
'''End the strips at the end of the scene or scene preview'''
scene = bpy.context.scene
if not scene.AL_objects:
return
frame_start, frame_end = bake_ops.frame_start_end(scene)
if scene.frame_current_final > frame_end:
frame_end = scene.frame_current_final + 10
frame_start = scene.frame_current_final if scene.frame_current_final < 0 else 0
# subscriptions_remove(handler = False)
for AL_item in scene.AL_objects:
obj = AL_item.object
if obj is None or obj not in scene.objects.values():
continue
#anim_data = anim_data_type(obj)
anim_datas = anim_layers.anim_datas_append(obj)
for anim_data in anim_datas:
if anim_data is None:
continue
if len(anim_data.nla_tracks) != len(obj.Anim_Layers):
continue
for layer, track in zip(obj.Anim_Layers, anim_data.nla_tracks):
if layer.frame_range:
continue
if len(track.strips) != 1:
continue
strip = track.strips[0]
strip.frame_start = frame_start
anim_layers.update_action_frame_range(frame_start, frame_end, layer, strip)
strip.scale = layer.speed
strip.frame_end = frame_end
# subscriptions_add(scene, handler = False)
#Subscribe to the scene frame_end
def subscribe_to_frame_end(scene):
'''subscribe_to_frame_end and frame preview end'''
subscribe_end = scene.path_resolve("frame_end", False)
subscribe_preview_end = scene.path_resolve("frame_preview_end", False)
subscribe_use_preview = scene.path_resolve("use_preview_range", False)
for subscribe in [subscribe_end, subscribe_preview_end, subscribe_use_preview]:
bpy.msgbus.subscribe_rna(
key=subscribe,
owner=scene,
args=(),
notify=frameend_update_callback,)
# bpy.msgbus.publish_rna(key=subscribe)
# def action_framestart_update_callback(*args):
# ''' update the strip start with the action start'''
def track_update_callback():
'''update layers with the tracks name'''
# global initial_call
# if initial_call:
# return
if not bpy.context.selected_objects:
return
obj = bpy.context.object
if obj is None:
return
if not obj.als.turn_on:
return
current_anim_data = anim_layers.anim_data_type(obj)
anim_datas = anim_layers.anim_datas_append(obj)
for anim_data in anim_datas:
if anim_data is None:
return
nla_tracks = anim_data.nla_tracks
if not len(nla_tracks):# or len(nla_tracks[:-1]) != len(obj.Anim_Layers):
return
override_tracks = anim_layers.check_override_tracks(obj, anim_data)
for i, track in enumerate(nla_tracks):
if anim_data != current_anim_data:
continue
#make sure there are no duplicated names
if track.name != obj.Anim_Layers[i].name:
#If its an override track, then make sure the reference object name is also synchronized
if obj.Anim_Layers[i].name in override_tracks:
override_tracks[obj.Anim_Layers[i].name].name = track.name
obj.Anim_Layers[i].name = track.name
if len(track.strips) == 1:
track.strips[0].name = track.name
def subscribe_to_track_name(scene):
'''Subscribe to the name of track'''
#subscribe_track = nla_track.path_resolve("name", False)
subscribe_track = (bpy.types.NlaTrack, 'name')
bpy.msgbus.subscribe_rna(
key=subscribe_track,
# owner of msgbus subcribe (for clearing later)
owner=scene,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=track_update_callback,)
# bpy.msgbus.publish_rna(key=subscribe_track)
def action_name_callback():
'''update layers with the tracks name'''
# global initial_call
# if initial_call:
# return
obj = bpy.context.object
if obj is None:
return
if not obj.als.turn_on:
return
anim_data = anim_layers.anim_data_type(obj)
#anim_datas = anim_layers.anim_datas_append(obj)
if anim_data is None:
return
nla_tracks = anim_data.nla_tracks
if not len(nla_tracks):
return
layer = obj.Anim_Layers[obj.als.layer_index]
if not len(nla_tracks[obj.als.layer_index].strips):
return
action = nla_tracks[obj.als.layer_index].strips[0].action
if action is None:
return
if not obj.als.auto_rename or layer.name == action.name:
return
layer.name = action.name
def subscribe_to_action_name(scene):
'''Subscribe to the name of track'''
#subscribe_track = nla_track.path_resolve("name", False)
subscribe_action = (bpy.types.Action, 'name')
bpy.msgbus.subscribe_rna(
key=subscribe_action,
# owner of msgbus subcribe (for clearing later)
owner=scene,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=action_name_callback,)
# bpy.msgbus.publish_rna(key=subscribe_action)
def influence_update_callback(*args):
'''update influence'''
# global initial_call
if not bpy.context.selected_objects:
return
obj = bpy.context.object
#checking if the object has nla tracks, when I used undo it was still calling the property on an object with no nla tracks
if obj is None:
return
if not obj.als.turn_on:
return
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
return
if not len(anim_data.nla_tracks):
return
track = anim_data.nla_tracks[obj.als.layer_index]
if len(track.strips) != 1:
return
if track.strips[0].fcurves[0].mute or track.strips[0].fcurves[0].lock:
return
# print('influence_update_callback')
#if the influence property and fcurve value are not the same then store the keyframes to check in the handler for a change
if track.strips[0].influence == track.strips[0].fcurves[0].evaluate(bpy.context.scene.frame_current):
return
global influence_keys
influence_keys = [tuple(key.co) for key in track.strips[0].fcurves[0].keyframe_points]
if bpy.context.scene.tool_settings.use_keyframe_insert_auto and len(track.strips[0].fcurves[0].keyframe_points):
track.strips[0].keyframe_insert('influence')
track.strips[0].fcurves[0].update()
return
def subscribe_to_influence(scene):
'''Subscribe to the influence of the track'''
subscribe_influence = (bpy.types.NlaStrip, 'influence')
bpy.msgbus.subscribe_rna(
key=subscribe_influence,
# owner of msgbus subcribe (for clearing later)
owner=scene,
# Args passed to callback function (tuple)
args=(scene,),
# Callback function for property update
notify=influence_update_callback,)
def subscribe_to_strip_settings(scene):
'''Subscribe to the strip settings of the track'''
frame_start = (bpy.types.NlaStrip, 'frame_start')
frame_end = (bpy.types.NlaStrip, 'frame_end')
action_frame_start = (bpy.types.NlaStrip, 'action_frame_start')
action_frame_end = (bpy.types.NlaStrip, 'action_frame_end')
scale = (bpy.types.NlaStrip, 'scale')
repeat = (bpy.types.NlaStrip, 'repeat')
attributes = [frame_start, frame_end, action_frame_start, action_frame_end, scale, repeat, frame_start, frame_end]
if bpy.app.version > (3, 2, 0):
#this properties exist only after Blender 3.2
frame_start_ui = (bpy.types.NlaStrip, 'frame_start_ui')
frame_end_ui = (bpy.types.NlaStrip, 'frame_end_ui')
attributes += [frame_start_ui, frame_end_ui]
for key in attributes:
bpy.msgbus.subscribe_rna(
key=key,
# owner of msgbus subcribe (for clearing later)
owner=scene,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=strip_settings_callback,)
def update_strip_layer_settings(strip, layer):
layer['speed'] = strip.scale
# start_offset = strip.action.frame_range[0] - strip.frame_start
# offset = (strip.action_frame_start - strip.frame_start - start_offset) * strip.scale + start_offset
# offset = (strip.action_frame_start - strip.frame_start) * strip.scale
if strip.repeat <= 1:
offset = (strip.frame_start * 1/strip.scale - strip.action_frame_start) * strip.scale
else:
#During repeat the offset is based on the distance from the action first keyframe
offset = strip.frame_start - strip.action.frame_range[0]
layer['offset'] = round(offset, 3)
#If custom frame range is turned off return to not lose frame range values
if not layer.frame_range:
return
layer['frame_end'] = strip.frame_end
layer['frame_start'] = strip.frame_start
layer['repeat'] = strip.repeat
def strip_settings_callback():
'''subscribe_to_strip_settings callback'''
if not bpy.context.selected_objects:
return
obj = bpy.context.object
if obj is None:
return
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
return
if not len(anim_data.nla_tracks):
return
if not len(obj.Anim_Layers):
return
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
sync_strip_range()
if not len(anim_data.nla_tracks[obj.als.layer_index].strips):
return
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
layer = obj.Anim_Layers[obj.als.layer_index]
update_strip_layer_settings(strip, layer)
anim_layers.redraw_areas([ 'VIEW_3D'])
+211 -119
View File
@@ -3,7 +3,7 @@ from . import anim_layers
from . import subscriptions
from mathutils import Vector, Quaternion
import numpy as np
#import time
# import time
def frame_start_end(scene):
if scene.use_preview_range:
@@ -12,7 +12,7 @@ def frame_start_end(scene):
else:
frame_start = scene.frame_start
frame_end = scene.frame_end
return frame_start, frame_end
return float(frame_start), float(frame_end)
def smart_start_end(smartkeys, frame_start, frame_end):
'''add the first and last frame of the scene if necessery'''
@@ -65,11 +65,11 @@ def smart_repeat(smartkeys, fcu, strip):
#duplicate the keys on the cycle after
keyframes_dup = []
for i in range(1, int(strip.repeat)):
for key in smartkeys[1:]:
keydup = smartkey(key)
keydup.frame += fcu_range*(i)
keydup.frame += round(fcu_range*(i), 2)
#duplicate the tangents tuple values
if hasattr(key, 'handle_left'):
keydup.handle_left = Vector([key.handle_left[0] + fcu_range*(i+1), key.handle_left[1]])
@@ -104,7 +104,6 @@ def smart_cycle(smartkeys, fcu, frame_start, frame_end):
cycle_end_dup = int((mod.frame_end - fcu.range()[1])/fcu_range)+2
#copy the the right handle of the first keyframe to the last, and the left handle from the last keyframe to the first
smartkeys[-1].handle_right_type = smartkeys[0].handle_left_type
smartkeys[0].handle_left_type = smartkeys[-1].handle_right_type
# if smartkeys[-1].interpolation == 'BEZIER':
@@ -113,13 +112,13 @@ def smart_cycle(smartkeys, fcu, frame_start, frame_end):
# if smartkeys[0].interpolation == 'BEZIER':
if hasattr(smartkeys[0], 'handle_left'):
smartkeys[0].handle_left = [smartkeys[-1].handle_left[0] - fcu_range, smartkeys[0].handle_left[1]]
#duplicate the keys on the cycle after
keyframes_dup = []
for key in smartkeys[1:]:
for i in range(cycle_end_dup):
keydup = smartkey(key)
keydup.frame += float(fcu_range*(i+1))
keydup.frame += float(round(fcu_range, 2)*(i+1))
if hasattr(keydup, 'handle_left'):
#duplicate the tangents tuple values
keydup.handle_left = Vector([key.handle_left[0] + fcu_range*(i+1), key.handle_left[1]])
@@ -127,8 +126,8 @@ def smart_cycle(smartkeys, fcu, frame_start, frame_end):
#if it's the last keyframe then the right handle get the value from the first keyframes
keydup.handle_right = Vector([key.handle_right[0] + fcu_range*(i+1), key.handle_right[1]])
if keydup not in keyframes_dup:
keydup.frame = round(keydup.frame, 2)
keyframes_dup.append(keydup)
#if it's an iternal cycle then duplicate the keyframes before the cycle keyframes
if not mod.cycles_before and mod.mode_before != 'None':
cycle_start_dup = int((fcu.range()[0] - frame_start) /fcu_range)+2
@@ -138,37 +137,52 @@ def smart_cycle(smartkeys, fcu, frame_start, frame_end):
cycle_start_dup = mod.cycles_before
if mod.use_restricted_range and mod.frame_start > (fcu.range()[0] + fcu_range * cycle_start_dup):
cycle_start_dup = int((fcu.range()[0]-mod.frame_start)/fcu_range)+2
#duplicate the keys on the cycle before
for key in smartkeys[:-1]:
for i in range(cycle_start_dup):
keydup = smartkey(key)
keydup.frame -= float(fcu_range*(i+1))
keydup.frame -= fcu_range*(i+1)
if hasattr(keydup, 'handle_left'):
#duplicate the tangents
keydup.handle_left = [key.handle_left[0] - fcu_range*(i+1), key.handle_left[1]]
if hasattr(keydup, 'handle_right'):
keydup.handle_right = [key.handle_right[0] - fcu_range*(i+1), key.handle_right[1]]
#if frame_end > key.frame > frame_start:
if keydup not in keyframes_dup:
keydup.frame = round(keydup.frame, 2)
keyframes_dup.append(keydup)
#merge the keyframes from the cycle with the or iginal keyframes
smartkeys.extend(keyframes_dup)
smartkeys = list(set(smartkeys))
smartkeys.sort()
if mod.use_restricted_range:
smartkeys = smart_start_end(smartkeys, mod.frame_start, mod.frame_end)
smartkeys = smart_start_end(smartkeys, mod.frame_start+1, mod.frame_end-1)
return smartkeys
def smart_bake(context):
#record all the keyframes into smartkeys
obj = context.object
frame_start, frame_end = context.scene.als.bake_range
fcu_smartkeys = {}
anim_data = anim_layers.anim_data_type(obj)
# Initialize the progress bar
wm = context.window_manager
total_iterations = 0
for track in anim_data.nla_tracks:
if track.mute:
continue
if len(track.strips) != 1 or track.strips[0].action is None:
continue
fcurves = anim_layers.get_fcurves(obj, track.strips[0].action)
total_iterations += len(fcurves)
wm.progress_begin(0, total_iterations)
processed = 0
for layer, track in zip(obj.Anim_Layers, anim_data.nla_tracks):
if track.mute:
continue
@@ -181,7 +195,8 @@ def smart_bake(context):
for strip_fcu in strip.fcurves:
strip_keyframes = [keyframe for keyframe in strip_fcu.keyframe_points if len(strip_fcu.keyframe_points) and not strip_fcu.mute]
for fcu in strip.action.fcurves:
fcurves = anim_layers.get_fcurves(obj, strip.action)
for fcu in fcurves:
if not fcu.is_valid or fcu.mute or selected_bones_filter(obj, fcu.data_path):
continue
smartkeys = []
@@ -192,9 +207,12 @@ def smart_bake(context):
if strip.blend_type in {'COMBINE', 'REPLACE'}:
keyframe.handle_left = Vector(key.handle_left)
keyframe.handle_right = Vector(key.handle_right)
keyframe.handle_left[0] = strip_start * layer.speed + (keyframe.handle_left[0] - strip_start) * layer.speed + layer.offset
keyframe.handle_right[0] = strip_start * layer.speed + (keyframe.handle_right[0] - strip_start) * layer.speed + layer.offset
#keyframe.frame += layer.offset
keyframe.frame = strip_start * layer.speed + (keyframe.frame - strip_start) * layer.speed + layer.offset
keyframe.frame = strip_start * layer.speed + (keyframe.frame - strip_start) * layer.speed + layer.offset# * layer.speed
keyframe.frame = round(keyframe.frame, 2)
if keyframe not in smartkeys:
smartkeys.append(keyframe)
@@ -210,37 +228,44 @@ def smart_bake(context):
smartkeys.sort()
if len(fcu.modifiers) and obj.als.mergefcurves:
smartkeys = smart_cycle(smartkeys, fcu, frame_start, frame_end)
#apply strip action settings
last_frame = (strip.frame_end - strip.frame_start) / strip.repeat + strip.frame_start
if strip.use_reverse:
for key in smartkeys:
key.frame = last_frame - (key.frame - strip.frame_start)
key.frame = last_frame - (key.frame - strip.frame_start)
if strip.repeat > 1:
smartkeys = smart_start_end(smartkeys, last_frame , last_frame+1) #strip.frame_start
smartkeys = remove_outofrange_keys(smartkeys, strip.frame_start, last_frame+1) #+ layer.offset
smartkeys = smart_repeat(smartkeys, fcu, strip)
if layer.frame_range:
if layer.custom_frame_range:
smartkeys = smart_start_end(smartkeys, strip.frame_start, strip.frame_end)
smartkeys = remove_outofrange_keys(smartkeys, strip.frame_start, strip.frame_end)
#if the strip is cutting with a different strip, then add keyframes in the cut
for layercut in obj.Anim_Layers:
if layercut.mute or not layercut.frame_range or layercut == layer:
if layercut.mute or not layercut.custom_frame_range or layercut == layer:
continue
if strip_start < layercut.frame_start < strip_end:
smartkeys = smart_start_end(smartkeys, (layercut.frame_start-1), strip.frame_end-1)
if strip_start < layercut.frame_end < strip_end:
smartkeys = smart_start_end(smartkeys, (layercut.frame_end+1), strip.frame_end-1)
#if the list of keyframes exists in a different track list then add them
if (fcu.data_path, fcu.array_index) in fcu_smartkeys:
smartkeys = list(set(fcu_smartkeys[(fcu.data_path, fcu.array_index)]+smartkeys))
#Merge all duplicated keyframes
smartkeys = list(set(smartkeys))
smartkeys.sort()
fcu_smartkeys.update({(fcu.data_path, fcu.array_index):smartkeys})
processed += 1
wm.progress_update(processed)
wm.progress_end()
#add inbetweens
for fcu, smartkeys in fcu_smartkeys.items():
if not smartkeys:
@@ -267,11 +292,13 @@ def add_inbetween(smartkeys):
key2 = smartkey()
key2.frame = smartkeys[i].frame + (smartkeys[i+1].frame - smartkeys[i].frame)*2/3
key1.frame = round(key1.frame, 2)
key2.frame = round(key2.frame, 2)
key2.inbetween = True
smartkeys.insert(i+1, key1)
smartkeys.insert(i+2, key2)
i += 3
return smartkeys
class smartkey:
@@ -345,7 +372,8 @@ def mute_modifiers(obj, nla_tracks):
for track in nla_tracks:
if len(track.strips) != 1 or track.strips[0].action is None:
continue
for fcu in track.strips[0].action.fcurves:
fcurves = anim_layers.get_fcurves(obj, track.strips[0].action)
for fcu in fcurves:
if selected_bones_filter(obj, fcu.data_path):
continue
if fcu.extrapolation == 'LINEAR':
@@ -370,7 +398,8 @@ def unmute_modifiers(obj, nla_tracks, modifier_rec):
for track in nla_tracks:
if track.strips[0].action is None:
continue
for fcu in track.strips[0].action.fcurves:
fcurves = anim_layers.get_fcurves(obj, track.strips[0].action)
for fcu in fcurves:
if not fcu.is_valid or selected_bones_filter(obj, fcu.data_path):
continue
if not len(fcu.modifiers):
@@ -398,7 +427,7 @@ def select_keyframed_bones(self, context, obj):
bpy.ops.object.posemode_toggle()
bpy.ops.pose.select_all(action='DESELECT')
for i in range(0, obj.als.layer_index+1):
obj.als.layer_index = i
obj.als['layer_index'] = i
anim_layers.select_layer_bones(self, context)
def mute_constraints(obj):
@@ -422,14 +451,15 @@ def smartbake_apply(obj, nla_tracks, fcu_keys, extrapolations):
# return
# action_range = strip.frame_end - strip.frame_start
for fcu in strip.action.fcurves:
fcurves = anim_layers.get_fcurves(obj, strip.action)
for fcu in fcurves:
if not fcu.is_valid:
continue
if selected_bones_filter(obj, fcu.data_path):
continue
fcu_key = (fcu.data_path, fcu.array_index)
if fcu_key not in fcu_keys.keys():
strip.action.fcurves.remove(fcu)
fcurves.remove(fcu)
continue
#get all the frames of the smart keys
smartkeys = fcu_keys[fcu_key]
@@ -453,34 +483,6 @@ def smartbake_apply(obj, nla_tracks, fcu_keys, extrapolations):
fcu.keyframe_points[-1].co = (smart_key.frame, value)
fcu.update()
#remove unnecessery keyframes
# for i in range(int(strip.action.frame_range[0]),int(strip.action.frame_range[1]+1)):
# if i in smart_frames:
# #get the index of the smart key based on the smart frames + interpolations
# smart_index = (smart_frames.index(i)+1)*3-3
# #if key was founded add the interpolation and handles
# for key in fcu.keyframe_points:
# if key.co[0] != i:
# continue
# key.co[1] = round(key.co[1], 4)
# key.interpolation = smartkeys[smart_index].interpolation
# # key.handle_left_type = smartkeys[smart_index].handle_left_type
# # key.handle_right_type = smartkeys[smart_index].handle_right_type
# key.handle_left_type = 'AUTO_CLAMPED' if smartkeys[smart_index].handle_left_type != 'VECTOR' else 'VECTOR'
# key.handle_right_type = 'AUTO_CLAMPED' if smartkeys[smart_index].handle_right_type != 'VECTOR' else 'VECTOR'
# break
#delete the keys that are not in the list
# else:
# if fcu.data_path.split(".")[-1] in transform_types:
# print('fcu.id_data', fcu.id_data, obj.animation_data.action)
# fcu.keyframe_delete(fcu.data_path.split(".")[-1] ,index = fcu_key[1], frame = i)
# else:
# try:
# fcu.keyframe_delete(fcu.data_path.split(".")[-1], frame = i)
# except TypeError:
# pass
i = 0
while i < len(fcu.keyframe_points):
key = fcu.keyframe_points[i]
@@ -518,7 +520,7 @@ def armature_restore(obj, b_layers, layers_rec, constraint_rec):
def attr_default(obj, fcu_key):
'''Returns the default value or default array value in a list'''
#check if the fcurve source belongs to a bone or obj
if fcu_key[0][:10] == 'pose.bones':
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]
@@ -557,7 +559,7 @@ def attr_default(obj, fcu_key):
attr = fcu_key[0].split('"')[1]
if 'attr' not in locals():
print(fcu_key[0], 'has no attributes returning 0')
# print(fcu_key[0], 'has no attributes returning 0')
return [0]
#since blender 3 access to custom property settings changed
@@ -571,6 +573,7 @@ def attr_default(obj, fcu_key):
return [0]
def selected_bones_filter(obj, fcu_data_path):
'''using obj.als.onlyselected property for the bake'''
if not obj.als.onlyselected:
return False
if obj.mode != 'POSE':
@@ -597,7 +600,6 @@ def evaluate_combine(data_path, added_array, eval_array, array_default, influenc
return eval_array
def frame_evaluation(frame, strip):
frame_eval = frame
#change the frame if the strip is on hold
if frame < strip.frame_start:
@@ -620,6 +622,17 @@ def frame_evaluation(frame, strip):
return frame_eval
def clean_no_user_slots(action):
'''Remove all the slots that are connected to the action and object but not part of the action slot'''
if not hasattr(action, 'slots'):
return
for slot in action.slots:
if slot is None:
continue
if len(slot.users()):
continue
action.slots.remove(slot)
def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, actioncopy, baked_layer = None):
@@ -627,19 +640,37 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
if obj is None:
return
anim_data = anim_layers.anim_data_type(obj)
baked_action = anim_data.action
if obj.als.operator == 'MERGE' and not additive and anim_data.action is not None: #and obj.als.onlyselected
# baked_action = anim_data.action
track = nla_tracks[obj.als.layer_index]
baked_action = track.strips[0].action
clean_no_user_slots(baked_action)
#create the baked fcurve
baked_channelbag = anim_layers.get_channelbag(obj, baked_action)
baked_fcurves = baked_channelbag.fcurves
if obj.als.operator == 'MERGE':# and not additive: # and anim_data.action is not None: #and obj.als.onlyselected
#overwrite action
anim_data.use_tweak_mode = False
#create a duplicate of the action on the merged layer to have a clean action in order to not write over the calculation
action_copy = bpy.data.actions[baked_action.name].copy()
nla_tracks[obj.als.layer_index].strips[0].action = action_copy
baked_action.id_root = obj.als.data_type
track.strips[0].action = action_copy
if hasattr(baked_action, 'id_root'):
baked_action.id_root = obj.als.data_type
blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
fcu_paths = []
# Initialize the progress bar
wm = bpy.context.window_manager
fcu_set = {fcu_key[0] for fcu_key in fcu_keys.keys()}
total_iterations = len(fcu_set)
wm.progress_begin(0, total_iterations) # (start, end range)
processed = 0
for fcu_key in fcu_keys.keys():
if fcu_key[0] in fcu_paths:
continue
@@ -674,18 +705,30 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
for track in nla_tracks:
if track.mute:
continue
if not len(track.strips):
continue
if track.strips[0].action is None:
continue
# if track == baked_layer:
# continue
#finding the channel group of array
fcu = track.strips[0].action.fcurves.find(fcu_key[0], index = i)
channelbag = anim_layers.get_channelbag(obj, track.strips[0].action)
if channelbag is None:
continue
fcurves = channelbag.fcurves
fcu = fcurves.find(fcu_key[0], index = i)
# print(f'track {track.name} fcurves {len(fcurves)}')
if fcu is None:
# print('fcu is none', fcu_key[0], track.name)
continue
group = fcu.group if fcu.group is not None else None
if group is not None:
if group.name in baked_action.groups:
group = baked_action.groups[group.name]
if group.name in baked_channelbag.groups:
group = baked_channelbag.groups[group.name]
else:
group = baked_action.groups.new(group.name)
group = baked_channelbag.groups.new(group.name)
#copy and append Modifiers into mod_list. Mute them if turned on
if len(fcu.modifiers) and not obj.als.mergefcurves:
@@ -697,20 +740,22 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
# modifier_rec.append(mod)
mod.mute = True
extrapolation = True if fcu.extrapolation == 'LINEAR' else False
#### Creating or overwritting (during merge) the new baked fcurves####
baked_fcu = ensure_fcurves_bversion(baked_fcurves, fcu_key[0], i)
#create the baked fcurve
baked_fcu = baked_action.fcurves.find(fcu_key[0], index = i)
if baked_fcu is not None:
baked_action.fcurves.remove(baked_fcu)
baked_fcu = baked_action.fcurves.new(fcu_key[0], index = i)
baked_fcu.color_mode = 'AUTO_RGB'
if group is not None:
if baked_fcu.group != group:
baked_fcu.group = group
if extrapolation:
baked_fcu.extrapolation = 'LINEAR'
baked_fcus.append(baked_fcu)
#select smart bake frame range or every frame in the range
if obj.als.smartbake:
#merge all the smartframe arrays
@@ -726,9 +771,15 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
#ITERATE through all the layers to evaluate
for track in nla_tracks:
if not len(track.strips):
continue
if track.mute or track == baked_layer or track.strips[0].action is None:
continue
strip = track.strips[0]
fcurves = anim_layers.get_fcurves(obj, strip.action)
if not len(fcurves):
continue
if (frame < strip.frame_start or frame > strip.frame_end) and strip.extrapolation == 'NOTHING':
layers_count += 1
continue
@@ -750,11 +801,14 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
added_array = []
missing = 0
for i in range(array_length):
fcu = strip.action.fcurves.find(fcu_key[0], index = i)
fcu = fcurves.find(fcu_key[0], index = i)
#if the fcurve is not found then get the default value
if fcu is None:
missing += 1
value = array_default[i] if blend_type in ('REPLACE', 'COMBINE') else 0
#getting the previous value if the fcurve is missing instead of just default
#the other option would be to use an array for the influence as well
value = eval_array[i] if blend_type in ('REPLACE', 'COMBINE') else 0
else:
value = fcu.evaluate(frame_eval)
added_array.append(value)
@@ -773,7 +827,6 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
eval_array = evaluate_combine(fcu_key[0], added_array, eval_array, array_default, influence)
else:
eval_array = eval('eval_array' + blend_types[blend_type] +' added_array' + '*' + str(influence))
#extrapolation = True if fcu.extrapolation == 'LINEAR' else False
layers_count += 1
if not eval_array.size:
@@ -792,18 +845,11 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
smartkey.value = eval_array[i]
if smartkey.inbetween:
continue
# if hasattr(smartkey, 'handle_left') and not additive:
# if not smartkey.handle_left[1]:
# smartkey.handle_left[1] = array_default[i]
# if hasattr(smartkey, 'handle_right') and not additive:
# if not smartkey.handle_right[1]:
# smartkey.handle_right[1] = array_default[i]
baked_fcu.keyframe_points.add(1)
keyframe = baked_fcu.keyframe_points[-1]
keyframe.co = (frame, eval_array[i])
for baked_fcu in baked_fcus:
if not len(baked_fcu.keyframe_points):
continue
@@ -818,12 +864,32 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
#paste the modifiers to the new baked fcurve
if i in mod_list and not len(baked_fcu.modifiers):
anim_layers.paste_modifiers(baked_fcu, mod_list[i])
processed += 1
wm.progress_update(processed)
wm.progress_end()
if not actioncopy and obj.als.operator == 'MERGE':
bpy.data.actions.remove(action_copy)
return baked_action
def ensure_fcurves_bversion(fcurves, data_path, i):
'''Either use ensure in Blender 5.0 and above or use find and new in older version'''
if hasattr(fcurves, 'ensure'):
baked_fcu = fcurves.ensure(data_path, index = i)
else:
baked_fcu = fcurves.find(data_path, index = i)
if baked_fcu is None:
baked_fcu = fcurves.new(data_path, index = i)
if len(baked_fcu.keyframe_points):
baked_fcu.keyframe_points.clear()
return baked_fcu
def non_recalc_handle_type(baked_keys):
handles_type = bpy.context.scene.als.handles_type
@@ -849,7 +915,9 @@ def add_interpolations(baked_fcu, smartkeys, layers_count = 0):
P2index = 2
if len(baked_keys) != len(keys):
print('unequal length of keys ',baked_fcu.data_path, len(baked_keys), len(keys))
print('set difference ', set([key.frame for key in keys]).difference(set([round(key.co[0], 2) for key in baked_keys])))
print('keys ', [key.frame for key in keys])
print('baked keys ', [round(key.co[0], 2) for key in baked_keys])
print('set difference ', set([key.frame for key in keys]).difference(set([key.co[0] for key in baked_keys])))
baked_keys[0].handle_left_type = 'VECTOR'
baked_keys[-1].handle_right_type = 'VECTOR'
@@ -876,7 +944,6 @@ def add_interpolations(baked_fcu, smartkeys, layers_count = 0):
skip = True
if not smartkeys[P1index].inbetween :
# print(baked_fcu.data_path, baked_keys[i].co[0], 'not with inbetween', P1index, P2index, i)
P1index += 1
P2index += 1
continue
@@ -911,11 +978,10 @@ def add_interpolations(baked_fcu, smartkeys, layers_count = 0):
#iterate through the inbetween smartkeys
P1index += 3
P2index += 3
# baked_fcu.update()
#add in-betweener
def apply_handle_types(baked_keys, smartkeys, i):
handles_type = bpy.context.scene.als.handles_type
@@ -933,35 +999,40 @@ def apply_handle_types(baked_keys, smartkeys, i):
def bake_range_type(self, context):
if self.bake_range_type == 'SCENE':
self.bake_range = frame_start_end(bpy.context.scene)
frame_start, frame_end = frame_start_end(context.scene)
self.bake_range = (int(frame_start), int(frame_end))
if self.bake_range_type == 'KEYFRAMES':
obj = context.object
anim_data = anim_layers.anim_data_type(obj)
frame_end = []
frame_start = []
posebones = context.selected_pose_bones
#if baking only selected bones then find the longest fcurves for the range
if obj.als.onlyselected:
posebones = context.selected_pose_bones
if obj.als.onlyselected and posebones:
bonespath = [bone.path_from_id() for bone in posebones]
#get the frame range from
if not bonespath:
return
for track in obj.animation_data.nla_tracks:
if not len(track.strips):
continue
action = track.strips[0].action
for fcu in action.fcurves:
for track in anim_data.nla_tracks:
if not len(track.strips):
continue
action = track.strips[0].action
if obj.als.onlyselected and posebones:
# Get fcurve range from the selected objects
fcurves = anim_layers.get_fcurves(obj, action)
for fcu in fcurves:
#check if the fcurve is in the selected bones
if any(path in fcu.data_path for path in bonespath):
frame_start.append(fcu.range()[0])
frame_end.append(fcu.range()[1])
else:
for track in obj.animation_data.nla_tracks:
if not len(track.strips):
continue
else:
# Get the action range
action = track.strips[0].action
frame_start.append(action.frame_range[0])
frame_end.append(action.frame_range[1])
# Checking for the longest action in all the actions
if frame_start:
self.bake_range = (int(min(frame_start)), int(max(frame_end)))
@@ -1042,10 +1113,23 @@ class MergeAnimLayerDown(bpy.types.Operator):
obj.als.smartbake = False
subscriptions.subscriptions_remove()
#start = time.time()
# start = time.time()
#define the start and end frame of the bake, according to scene or preview length
frame_start, frame_end = context.scene.als.bake_range
# Incase the strips are shorter then the keyframe range (because scene is shorter)
# Then updating the strips length
for layer, track in zip(obj.Anim_Layers, anim_data.nla_tracks):
if layer.custom_frame_range:
continue
if len(track.strips) != 1:
continue
strip = track.strips[0]
strip.frame_start =frame_start
anim_layers.update_action_frame_range(frame_start, frame_end, layer, strip)
strip.scale = layer.speed
strip.frame_end = frame_end
obj.als.view_all_keyframes = False
if context.scene.frame_current > frame_end:
context.scene.frame_current = frame_end
@@ -1054,8 +1138,9 @@ class MergeAnimLayerDown(bpy.types.Operator):
blendings = [track.strips[0].blend_type for track in nla_tracks[layer_index:] if len(track.strips) == 1]
#define if the new baked layer is going to be additive or replace
additive = False
if obj.als.direction == 'UP' and 'REPLACE' not in blendings and obj.als.baketype == 'AL':
additive = False
if obj.als.direction == 'UP' and 'REPLACE' not in blendings and obj.als.baketype == 'AL' and layer_index:
if 'COMBINE' in blendings:
blend = 'COMBINE'
else:
@@ -1063,15 +1148,19 @@ class MergeAnimLayerDown(bpy.types.Operator):
additive = True
else:
blend = 'REPLACE'
mute_rec = mute_unbaked_layers(layer_index, nla_tracks, additive)
fcu_keys = smart_bake(context)
if obj.als.operator == 'MERGE':
if obj.als.direction == 'DOWN':
obj.als.layer_index = 0
baked_layer = None
action = anim_data.nla_tracks[obj.als.layer_index].strips[0].action
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
action = strip.action
if hasattr(strip, 'action_slot'):
action_slot = strip.action_slot
if action is not None: action_name = action.name
#if baking to a new layer then setup the new index and layer
@@ -1109,7 +1198,8 @@ class MergeAnimLayerDown(bpy.types.Operator):
for col in obj.data.collections:
layers_rec.update({col : col.is_visible})
col.is_visible = True
# Select the bones from all the layers
self.shift = True
select_keyframed_bones(self, context, obj)
constraint_rec = mute_constraints(obj)
@@ -1123,7 +1213,7 @@ class MergeAnimLayerDown(bpy.types.Operator):
if not obj.select_get():
obj.select_set(True)
bpy.ops.nla.bake(frame_start = frame_start, frame_end = frame_end, only_selected = True, visual_keying=True, clear_constraints=obj.als.clearconstraints, bake_types = bake_type, step = self.step)
anim_data.action.fcurves.update()
# anim_data.action.fcurves.update()
strip = track.strips[0]
old_action = strip.action
@@ -1133,7 +1223,6 @@ class MergeAnimLayerDown(bpy.types.Operator):
track.strips.remove(strip)
strip = track.strips.new(track.name, 0, action)
anim_layers.tweak_mode_upper_stack(context, obj, anim_data, enter = False)
#strip.action = anim_data.action
if obj.als.smartbake:
smartbake_apply(obj, nla_tracks, fcu_keys, extrapolations)
@@ -1141,35 +1230,39 @@ class MergeAnimLayerDown(bpy.types.Operator):
armature_restore(obj, b_layers, layers_rec, constraint_rec)
unmute_modifiers(obj, nla_tracks, modifier_rec)
anim_data.action = None
# anim_data.action = None
#bpy.data.actions.remove(old_action)
if self.actioncopy:
old_action.name = action_name + '_old'
else:
bpy.data.actions.remove(old_action)
strip.action.name = action_name
# strip.action.name = action_name
if blendings.count('COMBINE') == len(blendings) and len(blendings) and obj.als.direction == 'UP':
track.strips[0].blend_type = 'COMBINE'
else: #use anim layers bake
#print(frame_start, frame_end)
action = AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, self.step, self.actioncopy, baked_layer)
#action = AL_bake(self, frame_start, frame_end, nla_tracks, fcu_keys, additive, baked_layer)
anim_layers.tweak_mode_upper_stack(context, obj, anim_data, enter = False)
track.strips.remove(track.strips[0])
strip = track.strips.new(track.name, 0, action)
strip.blend_type = blend
#track.strips[0].action = action
#removing layers after merge
if obj.als.operator == 'MERGE':
if hasattr(strip, 'action_slot'):
if action_slot in strip.action.slots.values():
strip.action_slot = action_slot
else:
# During NLA Bake slot doesn't exist need to find a new on
strip.action_slot = anim_layers.get_obj_slot(obj, action)
#reset layer settings
baked_layer = obj.Anim_Layers[obj.als.layer_index]
baked_layer.repeat, baked_layer.speed, baked_layer.offset = 1, 1, 0
strip.use_sync_length = False
if baked_layer.frame_range:
baked_layer.frame_range = False
if baked_layer.custom_frame_range:
baked_layer.custom_frame_range = False
baked_layer.frame_start = frame_start
baked_layer.frame_end = frame_end
@@ -1210,7 +1303,6 @@ class MergeAnimLayerDown(bpy.types.Operator):
track.mute = True
else:
track.mute = False
action.use_fake_user = True
anim_layers.register_layers(obj, nla_tracks)
+197 -90
View File
@@ -10,6 +10,46 @@ from mathutils import Quaternion
from . import bake_ops
from . import anim_layers
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]
source = obj.pose.bones[bone]
#in case of shapekey animation
elif fcu_key[0][:10] == 'key_blocks':
attr = fcu_key[0].split('"')[1]
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[fcu_key[1]]
return attrvalue
#in case of property on object
elif 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:
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]
@@ -50,10 +90,17 @@ def add_value(key, value):
apply_handles(key, handle_r, handle_l)
#calculate the difference between current value and the fcurve value
def add_diff(obj, fcurves, path, current_value, eval_array):
def add_diff(obj, fcurves, path, current_value, eval_array):
'''Get the difference value and add it to all selected keyframes'''
if eval_array is None:
return
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 not filter_properties(obj, fcu):
@@ -72,7 +119,9 @@ class ScaleValuesOp(bpy.types.Operator):
#reset the values for dragging
self.stop = False
scene = context.scene
scene.multikey['is_dragging'] = True
global is_dragging
is_dragging = True
self.avg_value = dict()
#dictionary of the keyframes and their original INITIAL values
self.keyframes_values = dict()
@@ -81,15 +130,16 @@ class ScaleValuesOp(bpy.types.Operator):
#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
action = obj.animation_data.action
for fcu in action.fcurves:
if obj.mode == 'POSE':
if bake_ops.selected_bones_filter(obj, fcu.data_path):
continue
fcurves = anim_layers.get_fcurves(obj, action)
for fcu in fcurves:
if anim_layers.selected_bones_filter(obj, fcu):
continue
if not filter_properties(obj, fcu):
continue
@@ -112,6 +162,10 @@ class ScaleValuesOp(bpy.types.Operator):
self.keyframes_avg_value.update({key : avg_value})
if not self.keyframes_avg_value:
if 'is_dragging' in globals():
del is_dragging
scene.multikey['scale'] = 1
anim_layers.redraw_areas(['VIEW_3D'])
return('CANCELLED')
context.window_manager.modal_handler_add(self)
@@ -119,41 +173,52 @@ class ScaleValuesOp(bpy.types.Operator):
def modal(self, context, event):
scene = context.scene
scale = scene.multikey.scale
global is_dragging
try:
scene = context.scene
scale = scene.multikey.scale
#Quit the modal operator when the slider is released
if self.stop:
del is_dragging
scene.multikey['scale'] = 1
anim_layers.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'}
#Quit the modal operator when the slider is released
if self.stop:
scene.multikey['is_dragging'] = False
scene.multikey['scale'] = 1
anim_layers.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
except Exception as e:
# Log the error
print("Error:", e)
self['scale'] = 1
self.stop = True
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'}
del is_dragging
return {'CANCELLED'}
def scale_value(self, context):
if 'is_dragging' in globals():
if is_dragging:
return
scene = context.scene
if scene.multikey.is_dragging:
return
obj = context.object
if obj is None:
@@ -168,7 +233,6 @@ def scale_value(self, context):
if context.mode == 'POSE' and not context.selected_pose_bones:
self['scale'] = 1
return
bpy.ops.anim.multikey_scale_value('INVOKE_DEFAULT')
def random_value(self, context):
@@ -177,10 +241,11 @@ def random_value(self, context):
if obj.animation_data.action is None:
continue
action = obj.animation_data.action
for fcu in action.fcurves:
if obj.mode == 'POSE':
if bake_ops.selected_bones_filter(obj, fcu.data_path):
continue
fcurves = anim_layers.get_fcurves(obj, action)
for fcu in fcurves:
# if obj.mode == 'POSE':
if anim_layers.selected_bones_filter(obj, fcu):
continue
if not filter_properties(obj, fcu):
continue
value_list = []
@@ -197,20 +262,42 @@ def random_value(self, context):
self['randomness'] = 0.1
def evaluate_array(action, fcu_path, frame, array_len):
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'''
fcu_array = []
array_len = len(array_default)
#assigning the default array in case
fcu_array = array_default.copy()
#get the missing arrays in case quaternion is not complete
for i in range(array_len):
fcu = action.fcurves.find(fcu_path, index = i)
fcu = fcurves.find(fcu_path, index = i)
if fcu is None:
continue
fcu_array.append(fcu.evaluate(frame))
if not len(fcu_array):
return None
fcu_array[i] = fcu.evaluate(frame)
# if (fcu_array == array_default).all():
# # print('295 return none')
# return None
return np.array(fcu_array)
def evaluate_layers(context, obj, anim_data, fcu, array_len):
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:
@@ -219,12 +306,11 @@ def evaluate_layers(context, obj, anim_data, fcu, array_len):
if not len(nla_tracks):
return None
frame = context.scene.frame_current
#blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
fcu_path = fcu.data_path
#array_default = np.array([bake_ops.attr_default(obj, (fcu_path, i)) for i in range(4) if anim_data.action.fcurves.find(fcu_path, index = i) is not None])
array_default = np.array(bake_ops.attr_default(obj, (fcu_path, fcu.array_index)))
eval_array = array_default
eval_array = array_default.copy()
for track in nla_tracks:
if track.mute:
continue
@@ -264,43 +350,49 @@ def evaluate_layers(context, obj, anim_data, fcu, array_len):
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
fcu_array = evaluate_array(action, fcu_path, frame_eval, array_len)
if fcu_array is None:
continue
###EVALUATION###
eval_array = evaluation(blend_type, fcu_path, fcu_array, eval_array, array_default, influence)
fcurves = anim_layers.get_fcurves(obj, action)
eval_array = evaluate_blend_type(fcurves, eval_array, fcu_path, frame_eval, influence, array_default, blend_type, blend_types)
#If there is an action on top of the nla tracks (not using anim layers) add it to the evaluation
if anim_data.action is not None and not anim_data.use_tweak_mode:
fcu_array = evaluate_array(anim_data.action, fcu_path, frame, array_len)
if fcu_array is not None:
eval_array = evaluation(anim_data.action_blend_type, fcu_path, fcu_array, eval_array, array_default, anim_data.action_influence)
return eval_array
#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
def evaluation(blend_type, fcu_path, fcu_array, eval_array, array_default, influence):
blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
# fcu_array = evaluate_array(action, fcu_path, frame_eval, array_len)
# if fcu_array is None:
# continue
action = anim_data.action
if action:
influence = anim_data.action_influence
blend_type = anim_data.action_blend_type
fcurves = anim_layers.get_fcurves(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 = bake_ops.evaluate_combine(fcu_path, fcu_array, eval_array, array_default, influence)
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):
for obj in context.selected_objects:
anim_data = obj.animation_data
@@ -310,36 +402,51 @@ def evaluate_value(self, context):
return
action = obj.animation_data.action
fcu_paths = []
# fcu_paths = []
transformations = ["rotation_quaternion","rotation_euler", "location", "scale"]
if obj.mode == 'POSE':
bonelist = context.selected_pose_bones if obj.als.onlyselected else obj.pose.bones
for fcu in action.fcurves:
if fcu in fcu_paths:
fcurves = anim_layers.get_fcurves(obj, action)
for fcu in fcurves:
# if fcu in fcu_paths:
# continue
current_value = None
if not filter_properties(obj, fcu):
continue
if obj.mode == 'POSE':
if bake_ops.selected_bones_filter(obj, fcu.data_path):
if anim_layers.selected_bones_filter(obj, fcu):
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
# transform = fcu.data_path[15 + len(bone.name):]
transform = fcu.data_path.split('"].')[1]
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)
eval_array = evaluate_layers(context, obj, anim_data, fcu, len(current_value))
#In case it was completly filtered out and not current value available
if not current_value:
continue
array_default = np.array(bake_ops.attr_default(obj, (fcu.data_path, fcu.array_index)))
eval_array = evaluate_layers(context, obj, anim_data, fcu, array_default)
if eval_array is None:
eval_array = evaluate_array(action, fcu.data_path, context.scene.frame_current, len(current_value))
fcurves = anim_layers.get_fcurves(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(obj, action.fcurves, fcu.data_path, np.array(current_value), eval_array)
add_diff(obj, fcurves, fcu.data_path, np.array(current_value), eval_array)
class MULTIKEY_OT_Multikey(bpy.types.Operator):
"""Edit all selected keyframes"""
@@ -362,7 +469,7 @@ class MultikeyProperties(bpy.types.PropertyGroup):
#handletype: bpy.props.BoolProperty(name="Keep handle types", description="Keep handle types", default=False, options={'HIDDEN'})
scale: bpy.props.FloatProperty(name="Scale Values Factor", description="Scale percentage from the average value", default=1.0, soft_max = 10, soft_min = -10, step=0.1, precision = 3, 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)
is_dragging: bpy.props.BoolProperty(default = False)
# is_dragging: bpy.props.BoolProperty(default = False)
#filters
filter_location: bpy.props.BoolVectorProperty(name="Location", description="Filter Location properties", default=(True, True, True), size = 3, options={'HIDDEN'})
+405 -194
View File
@@ -2,10 +2,26 @@ import bpy
from . import anim_layers
from . import bake_ops
import numpy as np
import time
import inspect
def subscriptions_remove(handler = True):
#clear all handlers and subsciptions
bpy.msgbus.clear_by_owner(bpy.context.scene)
# if scene is None : scene = bpy.context.scene
global subscriptions_owner
if 'subscriptions_owner' in globals():
bpy.msgbus.clear_by_owner(subscriptions_owner)
del subscriptions_owner
global influence_keys, selected_bones
if 'influence_keys' in globals():
del influence_keys
if 'selected_bones' in globals():
del selected_bones
if not handler:
return
if check_handler in bpy.app.handlers.depsgraph_update_pre:
@@ -14,31 +30,43 @@ def subscriptions_remove(handler = True):
bpy.app.handlers.frame_change_post.remove(animlayers_frame)
def subscriptions_add(scene, handler = True):
bpy.msgbus.clear_by_owner(scene)
global initial_call
initial_call = True
subscribe_to_frame_end(scene)
subscribe_to_track_name(scene)
subscribe_to_action_name(scene)
subscribe_to_strip_settings(scene)
subscribe_to_influence(scene)
global func_running
#If I call initial call from here it calls before running the previous functions
#initial_call = False
func_running = False
global subscriptions_owner
if 'subscriptions_owner' in globals():
bpy.msgbus.clear_by_owner(subscriptions_owner)
subscriptions_owner = object()
#Checking if frame range preview was turned on when pressing P
subscribe_to_preview_frame_end(scene)
subscribe_to_track_name(subscriptions_owner)
subscribe_to_action_name(subscriptions_owner)
subscribe_to_strip_settings(subscriptions_owner)
subscribe_to_influence(subscriptions_owner)
if bpy.app.version >= (4, 4, 0):
subscribe_to_action_slot(scene)
if not handler:
return
if check_handler not in bpy.app.handlers.depsgraph_update_pre:
bpy.app.handlers.depsgraph_update_pre.append(check_handler)
if animlayers_frame not in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.append(animlayers_frame)
def animlayers_frame(self, context):
scene = bpy.context.scene
current = scene.frame_current_final
def animlayers_frame(scene, context):
current = scene.frame_current_final
check_scene()
#During Particles bake screen attribute is empty
if bpy.context.screen is None:
return
#Make sure the animation is playing and not just running a motion path
if not bpy.context.screen.is_animation_playing:
return
#Checking if preview range was turned on or off, when using hotkey P it doesn't recognize
#only during the frame handler
if scene.get('framerange_preview') != scene.use_preview_range:
@@ -47,22 +75,28 @@ def animlayers_frame(self, context):
return
frame_start, frame_end = bake_ops.frame_start_end(scene)
# frame_start, frame_end = get_frame_range(scene)
reset_subscription = False
if 'outofrange' not in globals():
global outofrange
outofrange = False if frame_start <= current <= frame_end else True
if frame_start <= current <= frame_end:
outofrange = False if 0 <= current < frame_end else True
if 0 <= current < frame_end:
if outofrange:
frameend_update_callback()
outofrange = False
return
outofrange = True
if current <= frame_end:
return
#In case of running into empty objects then clean AL_objects
clean_AL_objects = False
#iterate only through objects with anim layers turned on
objects = [obj.object for obj in scene.AL_objects]
for obj in objects:
if obj is None:
clean_AL_objects = True
continue
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
return
@@ -74,63 +108,57 @@ def animlayers_frame(self, context):
continue
#checks if the layer has a custom frame range
if obj.Anim_Layers[i].frame_range:
layer = obj.Anim_Layers[i]
if layer.custom_frame_range:
continue
if not reset_subscription:
subscriptions_remove(handler = False)
reset_subscription = True
track.strips[0].frame_end_ui = current + 10
strip = track.strips[0]
if current < 0:
# anim_layers.strip_action_recalc(layer, track.strips[0])
strip.frame_start = current
# track.strips[0].action_frame_start = current * 1/layer.speed - layer.offset * 1/layer.speed
anim_layers.update_action_frame_range(current, frame_end, layer, strip)
strip.frame_end = frame_end + 10.0
elif current >= frame_end:
if strip.frame_start < 0:
strip.frame_start = 0
anim_layers.update_action_frame_range(0, frame_end, layer, strip)
anim_layers.update_action_frame_range(strip.frame_start, current + 10.0, layer, strip)
strip.frame_end = current + 10.0
if clean_AL_objects:
anim_layers.clean_AL_objects(scene)
if reset_subscription:
subscriptions_add(scene, handler = False)
sync_strip_range()
def objects_viewlayer(scene):
'''in case of an object excluded or included in the nla, update it because of an nla bug'''
if len(bpy.context.view_layer.objects) == scene.als.viewlayer_objects:
return
i = 0
while i < len(scene.AL_objects):
obj = scene.AL_objects[i].object
if obj is None:
scene.AL_objects.remove(i)
continue
i += 1
if obj.als.viewlayer and obj not in bpy.context.view_layer.objects.values():
obj.als.viewlayer = False
if not obj.als.viewlayer and obj in bpy.context.view_layer.objects.values():
#anim_data = anim_layers.anim_data_type(obj)
obj.als.upper_stack = False
obj.als.viewlayer = True
obj.als.layer_index = obj.als.layer_index
#anim_layers.tweak_mode_upper_stack(bpy.context, anim_data)
scene.als.viewlayer_objects = len(bpy.context.view_layer.objects)
def check_handler(self, context):
def check_handler(scene):
'''A main function that performs a series of checks using a handler'''
scene = bpy.context.scene
# scene = bpy.context.scene
#Timer for handler
# if 'last_check_time' not in globals():
# global last_check_time
# last_check_time = 0
# current_time = time.time()
# if current_time - last_check_time < 0.01:
# return
# last_check_time = current_time
#if there are no objects included in animation layers then return
if not len(scene.AL_objects):
return
objects_viewlayer(scene)
obj = bpy.context.object
#if the object was removed from the scene, then remove it from anim layers object list
if obj is None:
i = 0
while i < len(scene.AL_objects):
if scene.AL_objects[i].object not in scene.objects.values():
scene.AL_objects.remove(i)
else:
i += 1
anim_layers.clean_AL_objects(scene)
return
if not obj.als.turn_on:
return
anim_data = anim_layers.anim_data_type(obj)
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
return
if not anim_data.use_nla:
@@ -146,8 +174,6 @@ def check_handler(self, context):
active_action_update(obj, anim_data, nla_tracks)
#check if a keyframe was removed
if bpy.context.active_operator is not None:
if bpy.context.active_operator.name in ['Transform', 'Delete Keyframes'] and obj.als.edit_all_keyframes:
anim_layers.edit_all_keyframes()
if bpy.context.active_operator.name == 'Enter Tweak Mode':
if not bpy.context.active_operator.properties['use_upper_stack_evaluation']:
@@ -155,51 +181,76 @@ def check_handler(self, context):
if bpy.context.active_operator.name == 'Move Channels':
anim_layers.visible_layers(obj, nla_tracks)
# check if track and layers are synchronized
if len(nla_tracks) != len(obj.Anim_Layers):
new_layers_names = set(track.name for track in nla_tracks).difference(set(layer.name for layer in obj.Anim_Layers))
anim_layers.visible_layers(obj, nla_tracks)
if obj.als.layer_index > len(obj.Anim_Layers)-1:
obj.als.layer_index = len(obj.Anim_Layers)-1
#update new layer with strip settings
frame_start, frame_end = bake_ops.frame_start_end(bpy.context.scene)
for layer_name in new_layers_names:
if len(nla_tracks[layer_name].strips) != 1:
continue
strip = get_strip_in_meta(nla_tracks[layer_name].strips[0])
layer = obj.Anim_Layers[layer_name]
if (strip.frame_start, strip.frame_end) != (frame_start, frame_end):
layer['frame_range'] = True
update_strip_layer_settings(strip, layer)
layer['action'] = strip.action
# Making sure that scene.als.edit_all_layers_op is not somehow turned on
if not any(item.object.als.edit_all_keyframes for item in scene.AL_objects) and scene.als.edit_all_layers_op:
scene.als.edit_all_layers_op = False
# check if track and layers are synchronized
if track_layer_synchronization(obj, nla_tracks):
return
anim_layers.add_obj_to_animlayers(obj, [item.object for item in scene.AL_objects])
track = nla_tracks[obj.als.layer_index]
sync_frame_range(scene, track, layer)
# sync_strip_range(scene)
always_sync_range(track, layer)
# sync_strip_range(track, layer)
if anim_data.use_tweak_mode and layer.lock:
layer['lock'] = False
elif not anim_data.use_tweak_mode and not layer.lock:
layer['lock'] = True
influence_sync(obj, nla_tracks)
influence_sync(scene, obj, nla_tracks)
# continue if locked
if layer.lock:
return
#In case a keyframe was added and a new action slot was added to anim_data
#Check that it's synchornized with the strip action slot
strip = track.strips[0]
if hasattr(strip, 'action_slot') and strip.action:
if strip.action_slot != anim_data.action_slot:
strip.action_slot = anim_data.action_slot
if obj.als.view_all_keyframes:
anim_layers.hide_view_all_keyframes(obj, anim_data)
check_selected_bones(obj)
influence_check(nla_tracks[obj.als.layer_index])
def track_layer_synchronization(obj, nla_tracks):
'''check if track and layers are synchronized, running only when adding/removing tracks via the nla'''
if len(nla_tracks) == len(obj.Anim_Layers):
return False
new_layers_names = set(track.name for track in nla_tracks).difference(set(layer.name for layer in obj.Anim_Layers))
anim_layers.visible_layers(obj, nla_tracks)
if obj.als.layer_index > len(obj.Anim_Layers)-1:
obj.als.layer_index = len(obj.Anim_Layers)-1
#update new layer with strip settings
frame_start, frame_end = get_frame_range(bpy.context.scene)
for layer_name in new_layers_names:
if len(nla_tracks[layer_name].strips) != 1:
continue
strip = get_strip_in_meta(nla_tracks[layer_name].strips[0])
layer = obj.Anim_Layers[layer_name]
if not layer.custom_frame_range:
continue
if (strip.frame_start, strip.frame_end) != (frame_start, frame_end):
subscriptions_remove()
# print(f'strip.frame_start {strip.frame_start} strip.frame_end {strip.frame_end} frame_start {frame_start} frame_end {frame_end}')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return
update_strip_layer_settings(strip, layer)
layer['action'] = strip.action
return True
def active_action_update(obj, anim_data, nla_tracks):
'''updating the active action into the selected layer'''
if obj.Anim_Layers[obj.als.layer_index].lock:
@@ -227,16 +278,62 @@ def get_strip_in_meta(strip):
strip = strip.strips[0]
return strip
def sync_strip_range():
scene = bpy.context.scene
frame_start, frame_end = bake_ops.frame_start_end(scene)
def sync_frame_range(scene, track, layer):
'''Nla strips are not updating with msgbus when changing frame range in the ui
so it checks again during check handler if the frame range is changed and syncs it'''
if scene.frame_current_final > frame_end:
frame_end = scene.frame_current_final + 10
if bpy.context.screen.is_animation_playing:
return
# scene = bpy.context.scene
if not len(track.strips):
return
strip = track.strips[0]
#In case of Custom frame range
if layer['custom_frame_range']:
if (strip.frame_start, strip.frame_end) != (layer.frame_start, layer.frame_end):
update_strip_layer_settings(strip, layer)
else:
#In case of None custom frame range, make the strips adjust to scene frame range
frame_start, frame_end = get_frame_range(scene)
#defining global frame range to check if it was changed in the handler,
# msgbus subsciption is not updated before
if 'frame_range' not in globals():
global frame_range
frame_range = (frame_start, frame_end)
if frame_range != (frame_start, frame_end):
frame_range = (frame_start, frame_end)
frameend_update_callback()
return
#Turn on custom frame range if the current strip is not following the scene frame range
if (round(strip.frame_start, 2), round(strip.frame_end, 2)) != (round(frame_start, 2), round(frame_end, 2)):
subscriptions_remove()
# print('315 custom frame range')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return
def sync_strip_range(scene):
'''Checking all the strips if a value was changed in the nla (not including UI changes)
Similiar to sync custom frame range but iterating through all the layers
Currently disabled'''
frame_start, frame_end = get_frame_range(scene)
if 'frame_range' not in globals():
global frame_range
frame_range = (frame_start, frame_end)
clean_AL_objects = False
objects = [obj.object for obj in scene.AL_objects]
for obj in objects:
if obj is None:
#Turn on to clean AL_objects
clean_AL_objects = True
continue
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
continue
@@ -246,20 +343,31 @@ def sync_strip_range():
for i, track in enumerate(nla_tracks):
if len(track.strips) != 1:
continue
if obj.Anim_Layers[i]['frame_range']:
layer = obj.Anim_Layers[i]
if layer['custom_frame_range']:
if (strip.frame_start, strip.frame_end) != (layer.frame_start, layer.frame_end):
update_strip_layer_settings(strip, layer)
continue
strip = track.strips[0]
strip_frame_start = strip.frame_start
strip_frame_end = strip.frame_end
if (strip_frame_start, round(strip_frame_end)) != (0.0, float(frame_end)):
obj.Anim_Layers[i]['frame_range'] = True
if (strip_frame_start, round(strip_frame_end, 2)) != (frame_start, float(frame_end)):
subscriptions_remove()
# print('357 custom_frame_range_warning ')
# print(f'strip_frame_start {strip_frame_start} strip_frame_end {round(strip_frame_end, 2)} frame_start {frame_start} frame_end {float(frame_end)}')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return
if clean_AL_objects:
anim_layers.clean_AL_objects(scene)
def always_sync_range(track, layer):
'''sync frame range when always sync turned on'''
if not len(track.strips):
return
if not layer.frame_range:
if not layer.custom_frame_range:
if track.strips[0].use_sync_length:
track.strips[0].use_sync_length = False
return
@@ -273,7 +381,7 @@ def always_sync_range(track, layer):
anim_layers.sync_frame_range(bpy.context)
layer.action_range = strip.action.frame_range
def influence_sync(obj, nla_tracks):
def influence_sync(scene, obj, nla_tracks):
#Tracks that dont have keyframes are locked
for i, track in enumerate(nla_tracks):
@@ -287,56 +395,77 @@ def influence_sync(obj, nla_tracks):
if not len(track.strips[0].fcurves[0].keyframe_points):
#apply the influence property to the temp property when keyframes are removed (but its still locked)
if not track.strips[0].fcurves[0].lock:
obj.Anim_Layers[i].influence = track.strips[0].influence
# obj.Anim_Layers[i]['influence'] = track.strips[0].influence
scene.als['influence'] = track.strips[0].influence
track.strips[0].fcurves[0].lock = True
if obj.animation_data is None:
if scene.animation_data is None:
return
action = obj.animation_data.action
action = scene.animation_data.action
if action is None:
return
#if a keyframe was found in the temporary property then add it to the
data_path = 'Anim_Layers[' + str(obj.als.layer_index) + '].influence'
fcu_influence = action.fcurves.find(data_path)
# data_path = 'Anim_Layers[' + str(obj.als.layer_index) + '].influence'
data_path = 'als.influence'
fcurves = anim_layers.get_fcurves(scene, action, data_type = 'SCENE')
if not len(fcurves):
return
# fcurves = action.fcurves
fcu_influence = fcurves.find(data_path)
if fcu_influence is None:
return
if not len(fcu_influence.keyframe_points):
return
#remove the temporary influence
action.fcurves.remove(fcu_influence)
fcurves.remove(fcu_influence)
#if the action was created just for the influence because of empty object data type then remove the action
if action.name == obj.name + 'Action' and not len(obj.animation_data.nla_tracks) and not len(action.fcurves):
if action.name == scene.name + 'Action' and not len(scene.animation_data.nla_tracks) and not len(fcurves):
bpy.data.actions.remove(action)
if obj.Anim_Layers[obj.als.layer_index].influence_mute:
return
strip = nla_tracks[obj.als.layer_index].strips[0]
if strip.fcurves[0].mute:
return
strip.fcurves[0].lock = False
# if not strip.influence:
# strip.influence = 0.0001
strip.keyframe_insert('influence')
strip.fcurves[0].update()
def influence_check(selected_track):
'''update influence when a keyframe was added without autokey'''
#skip the next steps if a strip is missing or tracks were removed from the nla tracks
if len(selected_track.strips) != 1:# or obj.als.layer_index > len(nla_tracks)-2:
return
if not len(selected_track.strips[0].fcurves):
strip = selected_track.strips[0]
if not len(strip.fcurves):
return
global influence_keys
if selected_track.strips[0].fcurves[0].mute or not len(selected_track.strips[0].fcurves[0].keyframe_points) or bpy.context.scene.tool_settings.use_keyframe_insert_auto:
if strip.fcurves[0].mute or not len(strip.fcurves[0].keyframe_points) or bpy.context.scene.tool_settings.use_keyframe_insert_auto:
if 'influence_keys' in globals():
del influence_keys
return #when the fcurve doesnt have keyframes, or when autokey is turned on, then return
#update if the influence keyframes are changed. influence_keys are first added in influence_update_callback
if 'influence_keys' not in globals():
initialize_influence_keys(strip)
return
wm = bpy.context.window_manager
if not len(wm.operators):
return
if "ANIM_OT_keyframe_insert" not in wm.operators[-1].bl_idname:
return
if influence_keys != [tuple(key.co) for key in selected_track.strips[0].fcurves[0].keyframe_points]:
selected_track.strips[0].fcurves[0].update()
del influence_keys
length = len(strip.fcurves[0].keyframe_points)*2
keyframes = np.zeros(length)
strip.fcurves[0].keyframe_points.foreach_get('co', keyframes)
# Comparing only the values, because if it updates while duplicating or moving frames than it's crashing
if np.array_equal(influence_keys, keyframes):
return
selected_track.strips[0].fcurves[0].update()
influence_keys = keyframes
def check_selected_bones(obj):
'''running in the handler and checking if the selected bones were changed during view multiply layer keyframes'''
@@ -353,23 +482,50 @@ def check_selected_bones(obj):
selected_bones = bpy.context.selected_pose_bones
obj.als.view_all_keyframes = True
def check_scene():
'''update strip frame end after scene change, this is part of the animlayers_frame handler'''
if 'current_scene' not in globals():
global current_scene
current_scene = bpy.context.scene
return
if current_scene != bpy.context.scene:
#remove old scene from subscriptions
subscriptions_remove(handler = False)
frameend_update_callback()
current_scene = bpy.context.scene
#Add the new scene to subscriptions
subscriptions_add(current_scene, handler = False)
########################### MSGBUS SUBSCRIPTIONS #############################
#Callback function for Scene frame end
def get_frame_range(scene):
'''Getting the frame range also when outside of scene frame range'''
frame_start, frame_end = bake_ops.frame_start_end(scene)
#if it's out of range add 10 frames to the current frame, else add 10 frames to the scene frame end
frame_end = scene.frame_current_final + 10.0 if scene.frame_current_final >= frame_end else frame_end + 10.0
frame_start = scene.frame_current_final if scene.frame_current_final < 0 else 0.0
return frame_start, frame_end
def frameend_update_callback():
'''End the strips at the end of the scene or scene preview'''
scene = bpy.context.scene
if not scene.AL_objects:
return
frame_start, frame_end = bake_ops.frame_start_end(scene)
if scene.frame_current_final > frame_end:
frame_end = scene.frame_current_final + 10
#return
subscriptions_remove(handler = False)
frame_start, frame_end = get_frame_range(scene)
clean_AL_objects = False
#Iterating through all the tracks
for AL_item in scene.AL_objects:
obj = AL_item.object
if obj is None or obj not in scene.objects.values():
clean_AL_objects = True
continue
#anim_data = anim_data_type(obj)
anim_datas = anim_layers.anim_datas_append(obj)
@@ -380,32 +536,39 @@ def frameend_update_callback():
if len(anim_data.nla_tracks) != len(obj.Anim_Layers):
continue
for layer, track in zip(obj.Anim_Layers, anim_data.nla_tracks):
if layer.frame_range:
if layer.custom_frame_range:
continue
if len(track.strips) == 1:
track.strips[0].action_frame_start = 0 - layer.offset * 1/layer.speed
track.strips[0].action_frame_end = frame_end * 1/layer.speed - layer.offset * 1/layer.speed
track.strips[0].frame_start = 0
track.strips[0].frame_end = frame_end
track.strips[0].scale = layer.speed
if len(track.strips) != 1:
continue
strip = track.strips[0]
strip.frame_start = frame_start
anim_layers.update_action_frame_range(frame_start, frame_end, layer, strip)
strip.scale = layer.speed
strip.frame_end = frame_end
if clean_AL_objects:
anim_layers.clean_AL_objects(scene)
subscriptions_add(scene, handler = False)
#Subscribe to the scene frame_end
def subscribe_to_frame_end(scene):
'''subscribe_to_frame_end and frame preview end'''
subscribe_end = scene.path_resolve("frame_end", False)
def subscribe_to_preview_frame_end(scene):
'''subscribe_to_preview_frame_end and frame preview end'''
global subscriptions_owner
# subscribe_end = scene.path_resolve("frame_end", False)
# Subscribing to preview frame end since it's not registering in the depsgraph
subscribe_preview_end = scene.path_resolve("frame_preview_end", False)
subscribe_use_preview = scene.path_resolve("use_preview_range", False)
for subscribe in [subscribe_end, subscribe_preview_end, subscribe_use_preview]:
# print('subscribe_to_preview_frame_end')
for subscribe in [subscribe_preview_end, subscribe_use_preview]:
bpy.msgbus.subscribe_rna(
key=subscribe,
owner=scene,
owner=subscriptions_owner,
args=(),
notify=frameend_update_callback,)
bpy.msgbus.publish_rna(key=subscribe)
# bpy.msgbus.publish_rna(key=subscribe)
# def action_framestart_update_callback(*args):
# ''' update the strip start with the action start'''
@@ -413,11 +576,9 @@ def subscribe_to_frame_end(scene):
def track_update_callback():
'''update layers with the tracks name'''
global initial_call
if initial_call:
# initial_call = False
return
# global initial_call
# if initial_call:
# return
if not bpy.context.selected_objects:
return
obj = bpy.context.object
@@ -446,7 +607,7 @@ def track_update_callback():
if len(track.strips) == 1:
track.strips[0].name = track.name
def subscribe_to_track_name(scene):
def subscribe_to_track_name(subscriptions_owner):
'''Subscribe to the name of track'''
#subscribe_track = nla_track.path_resolve("name", False)
@@ -455,21 +616,19 @@ def subscribe_to_track_name(scene):
bpy.msgbus.subscribe_rna(
key=subscribe_track,
# owner of msgbus subcribe (for clearing later)
owner=scene,
owner=subscriptions_owner,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=track_update_callback,)
bpy.msgbus.publish_rna(key=subscribe_track)
# bpy.msgbus.publish_rna(key=subscribe_track)
def action_name_callback():
'''update layers with the tracks name'''
global initial_call
if initial_call:
# initial_call = False
return
# global initial_call
# if initial_call:
# return
obj = bpy.context.object
if obj is None:
return
@@ -492,7 +651,7 @@ def action_name_callback():
return
layer.name = action.name
def subscribe_to_action_name(scene):
def subscribe_to_action_name(subscriptions_owner):
'''Subscribe to the name of track'''
#subscribe_track = nla_track.path_resolve("name", False)
@@ -500,24 +659,22 @@ def subscribe_to_action_name(scene):
bpy.msgbus.subscribe_rna(
key=subscribe_action,
# owner of msgbus subcribe (for clearing later)
owner=scene,
owner=subscriptions_owner,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=action_name_callback,)
bpy.msgbus.publish_rna(key=subscribe_action)
# bpy.msgbus.publish_rna(key=subscribe_action)
def influence_update_callback(*args):
def influence_update_callback():
'''update influence'''
global initial_call
if initial_call:
initial_call = False
return
# global initial_call
if not bpy.context.selected_objects:
return
obj = bpy.context.object
scene = bpy.context.scene
#checking if the object has nla tracks, when I used undo it was still calling the property on an object with no nla tracks
if obj is None:
return
@@ -528,41 +685,93 @@ def influence_update_callback(*args):
return
if not len(anim_data.nla_tracks):
return
track = anim_data.nla_tracks[obj.als.layer_index]
i = obj.als.layer_index
track = anim_data.nla_tracks[i]
if len(track.strips) != 1:
return
strip = track.strips[0]
scene.als['influence'] = strip.influence
# obj.Anim_Layers[i]['influence'] = strip.influence
if track.strips[0].fcurves[0].mute or track.strips[0].fcurves[0].lock:
if strip.fcurves[0].mute or strip.fcurves[0].lock:
return
if bpy.context.scene.tool_settings.use_keyframe_insert_auto and len(track.strips[0].fcurves[0].keyframe_points):
track.strips[0].keyframe_insert('influence')
track.strips[0].fcurves[0].update()
if not len(track.strips[0].fcurves[0].keyframe_points):
return
# This is relevant only for autokey update
if not bpy.context.scene.tool_settings.use_keyframe_insert_auto:
return
#if the influence property and fcurve value are not the same then store the keyframes to check in the handler for a change
if track.strips[0].influence != track.strips[0].fcurves[0].evaluate(bpy.context.scene.frame_current):
global influence_keys
influence_keys = [tuple(key.co) for key in track.strips[0].fcurves[0].keyframe_points]
def subscribe_to_influence(scene):
if len(track.strips[0].fcurves[0].keyframe_points):
strip.keyframe_insert('influence')
strip.fcurves[0].update()
return
def initialize_influence_keys(strip):
'''Setting up the influence keys'''
global influence_keys
length = len(strip.fcurves[0].keyframe_points)*2
keyframes = np.zeros(length)
strip.fcurves[0].keyframe_points.foreach_get('co', keyframes)
influence_keys = keyframes
def subscribe_to_influence(subscriptions_owner):
'''Subscribe to the influence of the track'''
subscribe_influence = (bpy.types.NlaStrip, 'influence')
bpy.msgbus.subscribe_rna(
key=subscribe_influence,
# owner of msgbus subcribe (for clearing later)
owner=scene,
owner=subscriptions_owner,
# Args passed to callback function (tuple)
args=(scene,),
args=(),
# Callback function for property update
notify=influence_update_callback,)
bpy.msgbus.publish_rna(key=subscribe_influence)
def subscribe_to_action_slot(subscriptions_owner):
'''Subscribe to the influence of the track'''
subscribe_slot = (bpy.types.NlaStrip, 'action_slot')
def subscribe_to_strip_settings(scene):
bpy.msgbus.subscribe_rna(
key=subscribe_slot,
# owner of msgbus subcribe (for clearing later)
owner=subscriptions_owner,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=slot_update_callback,)
def slot_update_callback():
'''Always updating action slot in the active action when updated in the strip'''
if not bpy.context.selected_objects:
return
obj = bpy.context.object
if obj is None:
return
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
return
if anim_data.action is None:
return
if not len(anim_data.nla_tracks):
return
if not len(obj.Anim_Layers):
return
if not len(anim_data.nla_tracks[obj.als.layer_index].strips):
return
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
anim_data.action_slot = strip.action_slot
def subscribe_to_strip_settings(subscriptions_owner):
'''Subscribe to the strip settings of the track'''
frame_start = (bpy.types.NlaStrip, 'frame_start')
frame_end = (bpy.types.NlaStrip, 'frame_end')
action_frame_start = (bpy.types.NlaStrip, 'action_frame_start')
@@ -571,8 +780,8 @@ def subscribe_to_strip_settings(scene):
repeat = (bpy.types.NlaStrip, 'repeat')
attributes = [frame_start, frame_end, action_frame_start, action_frame_end, scale, repeat, frame_start, frame_end]
if bpy.app.version > (3, 2, 0):
if bpy.app.version >= (3, 3, 0):
#this properties exist only after Blender 3.2
frame_start_ui = (bpy.types.NlaStrip, 'frame_start_ui')
frame_end_ui = (bpy.types.NlaStrip, 'frame_end_ui')
@@ -582,23 +791,29 @@ def subscribe_to_strip_settings(scene):
bpy.msgbus.subscribe_rna(
key=key,
# owner of msgbus subcribe (for clearing later)
owner=scene,
owner=subscriptions_owner,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=strip_settings_callback,)
#bpy.msgbus.publish_rna(key=frame_start)
def update_strip_layer_settings(strip, layer):
layer['speed'] = strip.scale
if not strip.action:
return
if strip.repeat <= 1:
#Reversing the offset calculation based on the action start frame, strip start and scale
action_start = strip.action.frame_range[0]
offset = strip.frame_start - action_start - (strip.action_frame_start - action_start) * strip.scale
else:
#During repeat the offset is based on the distance from the action first keyframe
offset = strip.frame_start - strip.action.frame_range[0]
start_offset = strip.action.frame_range[0] - strip.frame_start
offset = (strip.action_frame_start - strip.frame_start - start_offset) * strip.scale + start_offset
layer['offset'] = round(-offset, 3)
layer['speed'] = strip.scale
layer['offset'] = round(offset, 3)
#If custom frame range is turned off return to not lose frame range values
if not layer.frame_range:
if not layer.custom_frame_range:
return
layer['frame_end'] = strip.frame_end
layer['frame_start'] = strip.frame_start
@@ -607,11 +822,6 @@ def update_strip_layer_settings(strip, layer):
def strip_settings_callback():
'''subscribe_to_strip_settings callback'''
global initial_call
if initial_call:
# initial_call = False
return
if not bpy.context.selected_objects:
return
obj = bpy.context.object
@@ -624,11 +834,12 @@ def strip_settings_callback():
return
if not len(obj.Anim_Layers):
return
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
sync_strip_range()
# sync_strip_range()
if not len(anim_data.nla_tracks[obj.als.layer_index].strips):
return
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
layer = obj.Anim_Layers[obj.als.layer_index]
update_strip_layer_settings(strip, layer)
anim_layers.redraw_areas([ 'VIEW_3D'])
anim_layers.redraw_areas([ 'VIEW_3D'])