Files
blender-portable-repo/scripts/addons/Animation_Layers/subscriptions.py
T
2026-03-17 14:58:51 -06:00

846 lines
31 KiB
Python

import bpy
from . import anim_layers
from . import bake_ops
import numpy as np
import time
import inspect
def subscriptions_remove(handler = True):
#clear all handlers and subsciptions
# if scene is None : scene = bpy.context.scene
global subscriptions_owner
if 'subscriptions_owner' in globals():
bpy.msgbus.clear_by_owner(subscriptions_owner)
del subscriptions_owner
global influence_keys, selected_bones
if 'influence_keys' in globals():
del influence_keys
if 'selected_bones' in globals():
del selected_bones
if not handler:
return
if check_handler in bpy.app.handlers.depsgraph_update_pre:
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):
global func_running
func_running = False
global subscriptions_owner
if 'subscriptions_owner' in globals():
bpy.msgbus.clear_by_owner(subscriptions_owner)
subscriptions_owner = object()
#Checking if frame range preview was turned on when pressing P
subscribe_to_preview_frame_end(scene)
subscribe_to_track_name(subscriptions_owner)
subscribe_to_action_name(subscriptions_owner)
subscribe_to_strip_settings(subscriptions_owner)
subscribe_to_influence(subscriptions_owner)
if bpy.app.version >= (4, 4, 0):
subscribe_to_action_slot(scene)
if not handler:
return
if check_handler not in bpy.app.handlers.depsgraph_update_pre:
bpy.app.handlers.depsgraph_update_pre.append(check_handler)
if animlayers_frame not in bpy.app.handlers.frame_change_post:
bpy.app.handlers.frame_change_post.append(animlayers_frame)
def animlayers_frame(scene, context):
current = scene.frame_current_final
check_scene()
#During Particles bake screen attribute is empty
if bpy.context.screen is None:
return
#Make sure the animation is playing and not just running a motion path
if not bpy.context.screen.is_animation_playing:
return
#Checking if preview range was turned on or off, when using hotkey P it doesn't recognize
#only during the frame handler
if scene.get('framerange_preview') != scene.use_preview_range:
scene['framerange_preview'] = scene.use_preview_range
frameend_update_callback()
return
frame_start, frame_end = bake_ops.frame_start_end(scene)
# frame_start, frame_end = get_frame_range(scene)
reset_subscription = False
if 'outofrange' not in globals():
global outofrange
outofrange = False if 0 <= current < frame_end else True
if 0 <= current < frame_end:
if outofrange:
frameend_update_callback()
outofrange = False
return
outofrange = True
#In case of running into empty objects then clean AL_objects
clean_AL_objects = False
#iterate only through objects with anim layers turned on
objects = [obj.object for obj in scene.AL_objects]
for obj in objects:
if obj is None:
clean_AL_objects = True
continue
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
return
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.custom_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 = current
# track.strips[0].action_frame_start = current * 1/layer.speed - layer.offset * 1/layer.speed
anim_layers.update_action_frame_range(current, frame_end, layer, strip)
strip.frame_end = frame_end + 10.0
elif current >= frame_end:
if strip.frame_start < 0:
strip.frame_start = 0
anim_layers.update_action_frame_range(0, frame_end, layer, strip)
anim_layers.update_action_frame_range(strip.frame_start, current + 10.0, layer, strip)
strip.frame_end = current + 10.0
if clean_AL_objects:
anim_layers.clean_AL_objects(scene)
if reset_subscription:
subscriptions_add(scene, handler = False)
def check_handler(scene):
'''A main function that performs a series of checks using a handler'''
# scene = bpy.context.scene
#Timer for handler
# if 'last_check_time' not in globals():
# global last_check_time
# last_check_time = 0
# current_time = time.time()
# if current_time - last_check_time < 0.01:
# return
# last_check_time = current_time
#if there are no objects included in animation layers then return
if not len(scene.AL_objects):
return
obj = bpy.context.object
#if the object was removed from the scene, then remove it from anim layers object list
if obj is None:
anim_layers.clean_AL_objects(scene)
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 == '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)
# Making sure that scene.als.edit_all_layers_op is not somehow turned on
if not any(item.object.als.edit_all_keyframes for item in scene.AL_objects) and scene.als.edit_all_layers_op:
scene.als.edit_all_layers_op = False
# check if track and layers are synchronized
if track_layer_synchronization(obj, nla_tracks):
return
track = nla_tracks[obj.als.layer_index]
sync_frame_range(scene, track, layer)
# sync_strip_range(scene)
always_sync_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(scene, obj, nla_tracks)
# continue if locked
if layer.lock:
return
#In case a keyframe was added and a new action slot was added to anim_data
#Check that it's synchornized with the strip action slot
strip = track.strips[0]
if hasattr(strip, 'action_slot') and strip.action:
if strip.action_slot != anim_data.action_slot:
strip.action_slot = anim_data.action_slot
if obj.als.view_all_keyframes:
anim_layers.hide_view_all_keyframes(obj, anim_data)
check_selected_bones(obj)
influence_check(nla_tracks[obj.als.layer_index])
def track_layer_synchronization(obj, nla_tracks):
'''check if track and layers are synchronized, running only when adding/removing tracks via the nla'''
if len(nla_tracks) == len(obj.Anim_Layers):
return False
new_layers_names = set(track.name for track in nla_tracks).difference(set(layer.name for layer in obj.Anim_Layers))
anim_layers.visible_layers(obj, nla_tracks)
if obj.als.layer_index > len(obj.Anim_Layers)-1:
obj.als.layer_index = len(obj.Anim_Layers)-1
#update new layer with strip settings
frame_start, frame_end = get_frame_range(bpy.context.scene)
for layer_name in new_layers_names:
if len(nla_tracks[layer_name].strips) != 1:
continue
strip = get_strip_in_meta(nla_tracks[layer_name].strips[0])
layer = obj.Anim_Layers[layer_name]
if not layer.custom_frame_range:
continue
if (strip.frame_start, strip.frame_end) != (frame_start, frame_end):
subscriptions_remove()
# print(f'strip.frame_start {strip.frame_start} strip.frame_end {strip.frame_end} frame_start {frame_start} frame_end {frame_end}')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return
update_strip_layer_settings(strip, layer)
layer['action'] = strip.action
return True
def active_action_update(obj, anim_data, nla_tracks):
'''updating the active action into the selected layer'''
if obj.Anim_Layers[obj.als.layer_index].lock:
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_frame_range(scene, track, layer):
'''Nla strips are not updating with msgbus when changing frame range in the ui
so it checks again during check handler if the frame range is changed and syncs it'''
if bpy.context.screen.is_animation_playing:
return
# scene = bpy.context.scene
if not len(track.strips):
return
strip = track.strips[0]
#In case of Custom frame range
if layer['custom_frame_range']:
if (strip.frame_start, strip.frame_end) != (layer.frame_start, layer.frame_end):
update_strip_layer_settings(strip, layer)
else:
#In case of None custom frame range, make the strips adjust to scene frame range
frame_start, frame_end = get_frame_range(scene)
#defining global frame range to check if it was changed in the handler,
# msgbus subsciption is not updated before
if 'frame_range' not in globals():
global frame_range
frame_range = (frame_start, frame_end)
if frame_range != (frame_start, frame_end):
frame_range = (frame_start, frame_end)
frameend_update_callback()
return
#Turn on custom frame range if the current strip is not following the scene frame range
if (round(strip.frame_start, 2), round(strip.frame_end, 2)) != (round(frame_start, 2), round(frame_end, 2)):
subscriptions_remove()
# print('315 custom frame range')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return
def sync_strip_range(scene):
'''Checking all the strips if a value was changed in the nla (not including UI changes)
Similiar to sync custom frame range but iterating through all the layers
Currently disabled'''
frame_start, frame_end = get_frame_range(scene)
if 'frame_range' not in globals():
global frame_range
frame_range = (frame_start, frame_end)
clean_AL_objects = False
objects = [obj.object for obj in scene.AL_objects]
for obj in objects:
if obj is None:
#Turn on to clean AL_objects
clean_AL_objects = True
continue
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
continue
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
layer = obj.Anim_Layers[i]
if layer['custom_frame_range']:
if (strip.frame_start, strip.frame_end) != (layer.frame_start, layer.frame_end):
update_strip_layer_settings(strip, layer)
continue
strip = track.strips[0]
strip_frame_start = strip.frame_start
strip_frame_end = strip.frame_end
if (strip_frame_start, round(strip_frame_end, 2)) != (frame_start, float(frame_end)):
subscriptions_remove()
# print('357 custom_frame_range_warning ')
# print(f'strip_frame_start {strip_frame_start} strip_frame_end {round(strip_frame_end, 2)} frame_start {frame_start} frame_end {float(frame_end)}')
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
return
if clean_AL_objects:
anim_layers.clean_AL_objects(scene)
def always_sync_range(track, layer):
'''sync frame range when always sync turned on'''
if not len(track.strips):
return
if not layer.custom_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(scene, 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
scene.als['influence'] = track.strips[0].influence
track.strips[0].fcurves[0].lock = True
if scene.animation_data is None:
return
action = scene.animation_data.action
if action is None:
return
#if a keyframe was found in the temporary property then add it to the
# data_path = 'Anim_Layers[' + str(obj.als.layer_index) + '].influence'
data_path = 'als.influence'
fcurves = anim_layers.get_fcurves(scene, action, data_type = 'SCENE')
if not len(fcurves):
return
# fcurves = action.fcurves
fcu_influence = fcurves.find(data_path)
if fcu_influence is None:
return
if not len(fcu_influence.keyframe_points):
return
#remove the temporary influence
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 == scene.name + 'Action' and not len(scene.animation_data.nla_tracks) and not len(fcurves):
bpy.data.actions.remove(action)
strip = nla_tracks[obj.als.layer_index].strips[0]
if strip.fcurves[0].mute:
return
strip.fcurves[0].lock = False
# if not strip.influence:
# strip.influence = 0.0001
strip.keyframe_insert('influence')
strip.fcurves[0].update()
def influence_check(selected_track):
'''update influence when a keyframe was added without autokey'''
#skip the next steps if a strip is missing or tracks were removed from the nla tracks
if len(selected_track.strips) != 1:# or obj.als.layer_index > len(nla_tracks)-2:
return
strip = selected_track.strips[0]
if not len(strip.fcurves):
return
global influence_keys
if strip.fcurves[0].mute or not len(strip.fcurves[0].keyframe_points) or bpy.context.scene.tool_settings.use_keyframe_insert_auto:
if 'influence_keys' in globals():
del influence_keys
return #when the fcurve doesnt have keyframes, or when autokey is turned on, then return
#update if the influence keyframes are changed. influence_keys are first added in influence_update_callback
if 'influence_keys' not in globals():
initialize_influence_keys(strip)
return
wm = bpy.context.window_manager
if not len(wm.operators):
return
if "ANIM_OT_keyframe_insert" not in wm.operators[-1].bl_idname:
return
length = len(strip.fcurves[0].keyframe_points)*2
keyframes = np.zeros(length)
strip.fcurves[0].keyframe_points.foreach_get('co', keyframes)
# Comparing only the values, because if it updates while duplicating or moving frames than it's crashing
if np.array_equal(influence_keys, keyframes):
return
selected_track.strips[0].fcurves[0].update()
influence_keys = keyframes
def check_selected_bones(obj):
'''running in the handler and checking if the selected bones were changed during view multiply layer keyframes'''
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
def check_scene():
'''update strip frame end after scene change, this is part of the animlayers_frame handler'''
if 'current_scene' not in globals():
global current_scene
current_scene = bpy.context.scene
return
if current_scene != bpy.context.scene:
#remove old scene from subscriptions
subscriptions_remove(handler = False)
frameend_update_callback()
current_scene = bpy.context.scene
#Add the new scene to subscriptions
subscriptions_add(current_scene, handler = False)
########################### MSGBUS SUBSCRIPTIONS #############################
#Callback function for Scene frame end
def get_frame_range(scene):
'''Getting the frame range also when outside of scene frame range'''
frame_start, frame_end = bake_ops.frame_start_end(scene)
#if it's out of range add 10 frames to the current frame, else add 10 frames to the scene frame end
frame_end = scene.frame_current_final + 10.0 if scene.frame_current_final >= frame_end else frame_end + 10.0
frame_start = scene.frame_current_final if scene.frame_current_final < 0 else 0.0
return frame_start, frame_end
def frameend_update_callback():
'''End the strips at the end of the scene or scene preview'''
scene = bpy.context.scene
if not scene.AL_objects:
return
subscriptions_remove(handler = False)
frame_start, frame_end = get_frame_range(scene)
clean_AL_objects = False
#Iterating through all the tracks
for AL_item in scene.AL_objects:
obj = AL_item.object
if obj is None or obj not in scene.objects.values():
clean_AL_objects = True
continue
#anim_data = anim_data_type(obj)
anim_datas = anim_layers.anim_datas_append(obj)
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.custom_frame_range:
continue
if len(track.strips) != 1:
continue
strip = track.strips[0]
strip.frame_start = frame_start
anim_layers.update_action_frame_range(frame_start, frame_end, layer, strip)
strip.scale = layer.speed
strip.frame_end = frame_end
if clean_AL_objects:
anim_layers.clean_AL_objects(scene)
subscriptions_add(scene, handler = False)
#Subscribe to the scene frame_end
def subscribe_to_preview_frame_end(scene):
'''subscribe_to_preview_frame_end and frame preview end'''
global subscriptions_owner
# subscribe_end = scene.path_resolve("frame_end", False)
# Subscribing to preview frame end since it's not registering in the depsgraph
subscribe_preview_end = scene.path_resolve("frame_preview_end", False)
subscribe_use_preview = scene.path_resolve("use_preview_range", False)
# print('subscribe_to_preview_frame_end')
for subscribe in [subscribe_preview_end, subscribe_use_preview]:
bpy.msgbus.subscribe_rna(
key=subscribe,
owner=subscriptions_owner,
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(subscriptions_owner):
'''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=subscriptions_owner,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=track_update_callback,)
# bpy.msgbus.publish_rna(key=subscribe_track)
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(subscriptions_owner):
'''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=subscriptions_owner,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=action_name_callback,)
# bpy.msgbus.publish_rna(key=subscribe_action)
def influence_update_callback():
'''update influence'''
# global initial_call
if not bpy.context.selected_objects:
return
obj = bpy.context.object
scene = bpy.context.scene
#checking if the object has nla tracks, when I used undo it was still calling the property on an object with no nla tracks
if obj is None:
return
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
i = obj.als.layer_index
track = anim_data.nla_tracks[i]
if len(track.strips) != 1:
return
strip = track.strips[0]
scene.als['influence'] = strip.influence
# obj.Anim_Layers[i]['influence'] = strip.influence
if strip.fcurves[0].mute or strip.fcurves[0].lock:
return
if not len(track.strips[0].fcurves[0].keyframe_points):
return
# This is relevant only for autokey update
if not bpy.context.scene.tool_settings.use_keyframe_insert_auto:
return
#if the influence property and fcurve value are not the same then store the keyframes to check in the handler for a change
if len(track.strips[0].fcurves[0].keyframe_points):
strip.keyframe_insert('influence')
strip.fcurves[0].update()
return
def initialize_influence_keys(strip):
'''Setting up the influence keys'''
global influence_keys
length = len(strip.fcurves[0].keyframe_points)*2
keyframes = np.zeros(length)
strip.fcurves[0].keyframe_points.foreach_get('co', keyframes)
influence_keys = keyframes
def subscribe_to_influence(subscriptions_owner):
'''Subscribe to the influence of the track'''
subscribe_influence = (bpy.types.NlaStrip, 'influence')
bpy.msgbus.subscribe_rna(
key=subscribe_influence,
# owner of msgbus subcribe (for clearing later)
owner=subscriptions_owner,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=influence_update_callback,)
def subscribe_to_action_slot(subscriptions_owner):
'''Subscribe to the influence of the track'''
subscribe_slot = (bpy.types.NlaStrip, 'action_slot')
bpy.msgbus.subscribe_rna(
key=subscribe_slot,
# owner of msgbus subcribe (for clearing later)
owner=subscriptions_owner,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=slot_update_callback,)
def slot_update_callback():
'''Always updating action slot in the active action when updated in the strip'''
if not bpy.context.selected_objects:
return
obj = bpy.context.object
if obj is None:
return
anim_data = anim_layers.anim_data_type(obj)
if anim_data is None:
return
if anim_data.action is None:
return
if not len(anim_data.nla_tracks):
return
if not len(obj.Anim_Layers):
return
if not len(anim_data.nla_tracks[obj.als.layer_index].strips):
return
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
anim_data.action_slot = strip.action_slot
def subscribe_to_strip_settings(subscriptions_owner):
'''Subscribe to the strip settings of the track'''
frame_start = (bpy.types.NlaStrip, 'frame_start')
frame_end = (bpy.types.NlaStrip, 'frame_end')
action_frame_start = (bpy.types.NlaStrip, 'action_frame_start')
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, 3, 0):
#this properties exist only after Blender 3.2
frame_start_ui = (bpy.types.NlaStrip, 'frame_start_ui')
frame_end_ui = (bpy.types.NlaStrip, 'frame_end_ui')
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=subscriptions_owner,
# Args passed to callback function (tuple)
args=(),
# Callback function for property update
notify=strip_settings_callback,)
def update_strip_layer_settings(strip, layer):
if not strip.action:
return
if strip.repeat <= 1:
#Reversing the offset calculation based on the action start frame, strip start and scale
action_start = strip.action.frame_range[0]
offset = strip.frame_start - action_start - (strip.action_frame_start - action_start) * strip.scale
else:
#During repeat the offset is based on the distance from the action first keyframe
offset = strip.frame_start - strip.action.frame_range[0]
layer['speed'] = strip.scale
layer['offset'] = round(offset, 3)
#If custom frame range is turned off return to not lose frame range values
if not layer.custom_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
# 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'])