2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
+299
View File
@@ -0,0 +1,299 @@
# ***** 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, 8, 8),
"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"}
if "bpy" in locals():
import importlib
if "bake_ops" in locals():
importlib.reload(bake_ops)
if "anim_layers" in locals():
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)
]
)
viewlayer_objects: bpy.props.IntProperty(name='View Layer Objects', description='checking if objects were turned on or off from the view Layers', default=0)
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'})
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
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.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,
def update_panel(self, context):
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),])
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="")
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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,9 @@
{
"last_check": "2025-05-28 13:56:15.908215",
"backup_date": "",
"update_ready": false,
"ignore": false,
"just_restored": false,
"just_updated": false,
"version_text": {}
}
@@ -0,0 +1,311 @@
# ***** 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
@@ -0,0 +1,17 @@
{
"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
@@ -0,0 +1,510 @@
# 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
@@ -0,0 +1,649 @@
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'])
File diff suppressed because it is too large Load Diff
+410
View File
@@ -0,0 +1,410 @@
# 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 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):
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
scene.multikey['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
for fcu in action.fcurves:
if obj.mode == 'POSE':
if bake_ops.selected_bones_filter(obj, fcu.data_path):
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:
return('CANCELLED')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modal(self, context, event):
scene = context.scene
scale = scene.multikey.scale
#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
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'}
def scale_value(self, context):
scene = context.scene
if scene.multikey.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
for fcu in action.fcurves:
if obj.mode == 'POSE':
if bake_ops.selected_bones_filter(obj, fcu.data_path):
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_array(action, fcu_path, frame, array_len):
'''Create an array from all the indexes'''
fcu_array = []
for i in range(array_len):
fcu = action.fcurves.find(fcu_path, index = i)
if fcu is None:
continue
fcu_array.append(fcu.evaluate(frame))
if not len(fcu_array):
return None
return np.array(fcu_array)
def evaluate_layers(context, obj, anim_data, fcu, array_len):
'''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
#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
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
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)
#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
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
###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)
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
for fcu in action.fcurves:
if fcu in fcu_paths:
continue
if obj.mode == 'POSE':
if bake_ops.selected_bones_filter(obj, fcu.data_path):
continue
for bone in bonelist:
#find the fcurve of the bone
if fcu.data_path.rfind(bone.name) != 12 or fcu.data_path[12 + len(bone.name)] != '"':
continue
# transform = fcu.data_path[15 + len(bone.name):]
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))
if eval_array is None:
eval_array = evaluate_array(action, fcu.data_path, context.scene.frame_current, len(current_value))
#calculate the difference between current value and the fcurve value
add_diff(obj, action.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
@@ -0,0 +1,634 @@
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)
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)
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)
#If I call initial call from here it calls before running the previous functions
#initial_call = False
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
#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 frame_start <= current <= frame_end else True
if frame_start <= 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
if obj.Anim_Layers[i].frame_range:
continue
if not reset_subscription:
subscriptions_remove(handler = False)
reset_subscription = True
track.strips[0].frame_end_ui = current + 10
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):
'''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
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
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 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
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 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
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)) != (0.0, 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'
fcu_influence = action.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)
#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):
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
#return
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:
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
#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:
# initial_call = False
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:
# initial_call = False
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 initial_call:
initial_call = False
return
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
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
#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):
'''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,)
bpy.msgbus.publish_rna(key=subscribe_influence)
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,)
#bpy.msgbus.publish_rna(key=frame_start)
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
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'''
global initial_call
if initial_call:
# initial_call = False
return
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'])