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'])