import bpy import os import numpy as np # import sys from bpy.app.handlers import persistent from . import bake_ops from . import subscriptions from . import addon_updater_ops from . import multikey @persistent def loadanimlayers(self, context): '''When loading a file check if the current selected object is with animlayers, if not then check if there is something else turned on''' scene = bpy.context.scene anim_layer_objects = [AL_item.object for AL_item in scene.AL_objects] #if the current object is not turned on, then check if another object is turned on subscribe = False #unhide and store all the related objects and collections because of bug prior to 3.6 col_hide_viewlayer, col_hide_viewport, hidden_objs = unhide_collections_on_load(anim_layer_objects) for obj in bpy.data.objects: if obj is None: continue if obj.als.turn_on and len(obj.users_scene): add_obj_to_animlayers(obj, anim_layer_objects) #Make sure layer index is not more then the layers if obj.als.layer_index > len(obj.Anim_Layers) - 1: obj.als['layer_index'] = len(obj.Anim_Layers) - 1 subscribe = True if obj in bpy.context.view_layer.objects.values(): start_animlayers(obj) else: load_none_view_layer(obj) elif obj in anim_layer_objects: obj.als.turn_on = False scene.AL_objects.remove(anim_layer_objects.index(obj)) anim_layer_objects.remove(obj) hide_collections_on_load(col_hide_viewlayer, col_hide_viewport, hidden_objs) if subscribe: subscriptions.subscriptions_remove() subscriptions.subscriptions_add(scene) def turn_animlayers_on(self, context): '''Turning on and off the NLA with obj.als.turn_on property''' obj = self.id_data scene = context.scene anim_data = anim_data_type(obj) #iterate through all selected objects, in case both were checked with alt + click if obj is None: return if self.turn_on: check_anim_data_start(self, obj, anim_data) #workaround for issues coming from version 4.1.0 if bpy.app.version == (4, 1, 0): if scene.keying_sets.active is None: scene.keying_sets.active = scene.keying_sets_all['Location, Rotation & Scale'] scene.tool_settings.use_keyframe_insert_keyingset = True else: #Remove object from animlayers collection i = 0 while i < len(scene.AL_objects): if scene.AL_objects[i].object == obj or not scene.AL_objects[i].object: scene.AL_objects.remove(i) else: i += 1 anim_data = anim_data_type(obj) if anim_data is None: #continue return tweak_mode_upper_stack(context, obj, anim_data, enter = False) anim_data.use_nla = False #iterate only over object animation, not shapekeys and apply the last replace layer for track in anim_data.nla_tracks: if not len(track.strips) or track.mute: continue #Deselect all the strips to avoid entering tweak mode from other objects track.strips[0].select = False #Assign the base layer to the active action if not anim_data.action: anim_data.action = track.strips[0].action anim_data.action_blend_type = track.strips[0].blend_type #if there are no objects in AL_objects then subsciptions will be removed if not len(scene.AL_objects): obj.als.upper_stack = False #remove subscription only if there is no AL_objects in all the scene for scene in bpy.data.scenes: if len(scene.AL_objects): return if 'framerange_preview' in scene: del scene['framerange_preview'] subscriptions.subscriptions_remove() def find_anim_datas(obj): anim_datas = [] if hasattr(obj, 'animation_data'): anim_datas.append(obj.animation_data) if hasattr(obj.data, 'shape_keys'): if hasattr(obj.data.shape_keys, 'animation_data'): anim_datas.append(obj.data.shape_keys.animation_data) return anim_datas def check_anim_data_start(self, obj, selected_anim_data): '''adds subtract layer and active action of the first layer to animation data that is currently not selected''' anim_datas = find_anim_datas(obj) #adding a boolean to check if the addon started already during cleanup start = False for anim_data in anim_datas: if not hasattr(anim_data, 'nla_tracks'): continue if anim_data != selected_anim_data: continue if len(obj.Anim_Layers) > len(anim_data.nla_tracks): obj.Anim_Layers.clear() if not len(obj.Anim_Layers) and len(anim_data.nla_tracks): start = True #Turn off until it gets a confirmation how to proceed self['turn_on'] = False bpy.ops.anim.clear_nla_warning('INVOKE_DEFAULT') continue if len(obj.Anim_Layers) and not action_search(anim_data.action, anim_data.nla_tracks) and anim_data.action: start = True self['turn_on'] = False bpy.ops.anim.clear_active_action_warning('INVOKE_DEFAULT') continue if check_override_tracks(obj, anim_data) or check_override_layers(obj): bpy.ops.message.layersoverride('INVOKE_DEFAULT') # continue if len(obj.Anim_Layers) and obj.als.layer_index > len(obj.Anim_Layers) - 1: obj.als['layer_index'] = len(obj.Anim_Layers) - 1 if not start: subscriptions.subscriptions_remove() start_animlayers(obj) subscriptions.subscriptions_add(bpy.context.scene) def remove_old_setup(obj): 'remove subtract track from the old addon setup' anim_datas = find_anim_datas(obj) for anim_data in anim_datas: if anim_data is None: continue if not len(anim_data.nla_tracks): continue if len(anim_data.nla_tracks) == len(obj.Anim_Layers): continue subtract = anim_data.nla_tracks[-1] if len(subtract.strips) != 1: continue if subtract.strips[0].blend_type == 'SUBTRACT' and len(anim_data.nla_tracks) > len(obj.Anim_Layers): tweak_mode_upper_stack(bpy.context, obj, anim_data, enter = False) anim_data.action = None anim_data.nla_tracks.remove(subtract) def load_none_view_layer(obj): '''fix an nla bug which happens when a file is loaded and the object is excluded from view layer''' anim_data = anim_data_type(obj) if not len(obj.Anim_Layers) or not len(anim_data.nla_tracks): return i = obj.als.layer_index if obj.Anim_Layers[i].lock: return obj.als.viewlayer = False if not anim_data.use_tweak_mode: obj.als.upper_stack = False anim_data.use_tweak_mode = True def start_animlayers(obj): scene = bpy.context.scene AnimLayer_objects = [AnimLayers.object for AnimLayers in scene.AL_objects] remove_old_setup(obj) if obj not in AnimLayer_objects: add_obj_to_animlayers(obj, AnimLayer_objects) anim_data = anim_data_type(obj) if not hasattr(anim_data, 'nla_tracks'): return if not anim_data.use_nla: anim_data.use_nla = True if not len(anim_data.nla_tracks): return anim_data.nla_tracks[0].is_solo = False nla_tracks = anim_data.nla_tracks #check for tracks with duplicated names and assign with unique name track_names = [track.name for track in nla_tracks] for i, name in enumerate(track_names): if track_names.count(name) > 1: track_names[i] = unique_name(track_names, name) nla_tracks[i].name = track_names[i] if len(nla_tracks[i].strips) == 1: nla_tracks[i].strips[0].name = track_names[i] register_layers(obj, nla_tracks) #synchronize the temporary influence prorpery for i, layer in enumerate(obj.Anim_Layers): if len(nla_tracks[i].strips) != 1: continue if layer.influence != nla_tracks[i].strips[0].influence and layer.influence != -1: layer.influence = nla_tracks[i].strips[0].influence strip = nla_tracks[i].strips[0] if strip.action is None: continue layer.action_range = strip.action.frame_range if len(obj.Anim_Layers): obj.als.upper_stack = False #run layer updates obj.als.layer_index = 0 if obj.als.layer_index < 0 else obj.als.layer_index # obj.als.layer_index = obj.als.layer_index def add_obj_to_animlayers(obj, anim_layer_objects): '''Add the current object to the scene animation layers''' if obj in anim_layer_objects or obj is None or not obj.als.turn_on: return new_obj = bpy.context.scene.AL_objects.add() new_obj.object = obj new_obj.name = new_obj.object.name #anim_data = anim_data_type(obj) def register_layers(obj, nla_tracks): visible_layers(obj, nla_tracks) #apply the correct setup for the strips. If there are more then one strip then lock the layer for i, track in enumerate(nla_tracks): if len(track.strips) != 1 or track.strips[0].type == 'META' and len(obj.Anim_Layers) > i+1: obj.Anim_Layers[i].lock = True continue strip = track.strips[0] #strip.use_sync_length = False use_animated_influence(strip) if not len(strip.fcurves[0].keyframe_points): obj.Anim_Layers[i].influence = strip.influence #updating the ui list with the nla track names def visible_layers(obj, nla_tracks): '''Creates a list of all the tracks without the top subtrack for the UI List''' # mute = [] lock = [] # solo = [] influence_mute = [] frame_range = [] #store all the layer properties for layer in obj.Anim_Layers: # mute.append(layer.mute) lock.append(layer.lock) # solo.append(layer.solo) influence_mute.append(layer.influence_mute) frame_range.append((layer.frame_start, layer.frame_end, layer.speed, layer.repeat, layer.offset, layer.frame_range)) #check if a layer was removed and adjust the stored properties if len(nla_tracks) < len(obj.Anim_Layers): removed = 0 for i, layer in enumerate(obj.Anim_Layers): if layer.name not in nla_tracks: # mute.pop(i - removed) lock.pop(i - removed) # solo.pop(i - removed) influence_mute.pop(i - removed) frame_range.pop(i - removed) removed += 1 #check if a layer was added and adjust the stored properties if len(nla_tracks) > len(obj.Anim_Layers): obj.Anim_Layers.update() for i, track in enumerate(nla_tracks): if track.name not in obj.Anim_Layers: # mute.insert(i, track.mute) lock.insert(i, False) # solo.insert(i, False) influence_mute.insert(i, False) frame_range.insert(i, (0, 0, 1, 1, 0, False)) #write layers obj.Anim_Layers.clear() #check if there are still layers because of overrides length = len(obj.Anim_Layers) for i, track in enumerate(nla_tracks): if length > i: continue layer = obj.Anim_Layers.add() layer['name'] = track.name layer['mute'] = track.mute if len(track.strips) == 1: track.strips[0].name = track.name if lock: #check if the list is appended layer['lock'] = lock[i] if len(track.strips) and track.strips[0].action != None: layer['action'] = track.strips[0].action layer['influence_mute'] = influence_mute[i] layer['frame_start'] = frame_range[i][0] layer['frame_end'] = frame_range[i][1] layer['speed'] = frame_range[i][2] layer['repeat'] = frame_range[i][3] layer['offset'] = frame_range[i][4] layer['frame_range'] = frame_range[i][5] def use_animated_influence(strip): if strip.use_animated_influence: return strip.use_animated_influence = True strip.keyframe_delete(strip.fcurves[0].data_path, frame=0) strip.influence = 1 def check_override_layers(obj): if obj.override_library is None: return False if len(obj.override_library.reference.Anim_Layers): return True return False def check_override_tracks(obj, anim_data): if obj.override_library is None: return [] if anim_data is None: return [] if anim_data == obj.animation_data: anim_data_ref = obj.override_library.reference.animation_data elif anim_data == obj.data.shape_keys.animation_data: anim_data_ref = obj.override_library.reference.data.shape_keys.animation_data if anim_data_ref is None: return [] if len(anim_data_ref.nla_tracks): return anim_data_ref.nla_tracks else: return [] def check_overrides_ALobjects(obj): #check if an override object was added and already had animlayers turned on if not obj.override_library: return scene = bpy.context.scene if obj.name in scene.AL_objects: return if not scene.AL_objects: subscriptions.subscriptions_add(scene) anim_layer_objects = [AL_item.object for AL_item in scene.AL_objects] add_obj_to_animlayers(obj, anim_layer_objects) #################################################### Multiply layer view FUNCTIONS ############################################################################ def get_fcu_layer_keyframes(obj, context, track): keyframes = [] #store all the keyframe locations from the fcurves of the layer for fcu in track.strips[0].action.fcurves: if fcu.group is not None: if fcu.group.name == 'Anim Layers': continue #if only selected bones is used then check for the bones if obj.als.only_selected_bones and obj.mode == 'POSE': selected_bones = [bone.path_from_id() for bone in context.selected_pose_bones] if fcu.data_path.split('].')[0]+']' not in selected_bones: continue keyframes = store_layer_frames(fcu, keyframes) return sorted(set(keyframes)) def store_layer_frames(fcu, keyframes): '''storing the time also as the value, to be used for edit all keyframes''' length = len(fcu.keyframe_points)*2 new_keyframes = np.zeros(length) fcu.keyframe_points.foreach_get('co', new_keyframes) keyframes = np.concatenate((keyframes, new_keyframes[::2])) return keyframes def hide_view_all_keyframes(obj, anim_data): '''hide view all keyframes in the graph editor, to avoid the user changing the values and lock channels when edit all keyframes is turned off''' if anim_data.action is None: return if not len(anim_data.action.fcurves): return if obj.als.edit_all_keyframes: return if 'Anim Layers' in anim_data.action.groups: if anim_data.action.groups['Anim Layers'].lock: return anim_data.action.groups['Anim Layers'].lock = True return #if the group was not found or renamed iterate over the layers for i, layer in enumerate(obj.Anim_Layers): if layer.lock or obj.als.layer_index == i: continue fcu = anim_data.action.fcurves.find(layer.name, index = i) if fcu is None: continue if not fcu.group.lock: #lock the groups if edit is not selected fcu.group.lock = True fcu.group.name = 'Anim Layers' if bpy.context.area: if bpy.context.area.type != 'GRAPH_EDITOR': #hide the channels when using graph editor return if not fcu.hide: fcu.hide = True def fcurve_bones_path(obj, fcu): '''if only selected bones is used then check for the bones path in the fcurves data path''' if obj.als.only_selected_bones and obj.mode == 'POSE': selected_bones_path = [bone.path_from_id() for bone in bpy.context.selected_pose_bones] if fcu.data_path.split('].')[0]+']' not in selected_bones_path: return True return False def edit_all_keyframes(): obj = bpy.context.object anim_data = anim_data_type(obj) global fcu_layers for i, layer in enumerate(obj.Anim_Layers): #look for the Anim Layers fcurve if layer.lock or anim_data.action is None or i == obj.als.layer_index: continue fcu_layer = anim_data.action.fcurves.find(layer.name, index = i) if fcu_layer is None or not len(fcu_layer.keyframe_points): continue #Get the frames and keyframes to compare frames = sorted(store_layer_frames(fcu_layer, [])) frames_keys = dict(zip(frames, fcu_layer.keyframe_points)) if fcu_layers[fcu_layer.data_path] == frames_keys: continue #find the frames that were changed using sets changed_frames = set(fcu_layers[fcu_layer.data_path].keys()).difference(set(frames)) #check if keyframes were deleted if len(fcu_layers[fcu_layer.data_path]) != len(frames_keys) and bpy.context.active_operator.name == 'Delete Keyframes': #delete the relative keyframes in the action for fcurve in anim_data.nla_tracks[i].strips[0].action.fcurves: if fcurve_bones_path(obj, fcurve): continue if fcurve.group is not None: if fcurve.group.name == 'Anim Layers': continue #del_keyframes = [keyframe for keyframe in fcurve.keyframe_points if keyframe.co[0] in del_keys] keyframe_points = list(fcurve.keyframe_points) while keyframe_points: # remove the keyframes from the original action if keyframe_points[0].co[0] in changed_frames: fcurve.keyframe_points.remove(keyframe_points[0]) keyframe_points = list(fcurve.keyframe_points) else: keyframe_points.pop(0) fcurve.update() fcu_layers.update({fcu_layer.data_path : frames_keys}) continue # memory_usage_bytes = sys.getsizeof(fcu_layers) # memory_usage_kb = memory_usage_bytes / 1024 # print('memory_usage_kb', memory_usage_kb) #check the new frame number of the keyframes old_keys = {} for changed_frame in changed_frames: key = fcu_layers[fcu_layer.data_path][changed_frame] old_keys.update({changed_frame : key.co[0]}) fcu_layers.update({fcu_layer.data_path : frames_keys}) if not old_keys: continue #iterate through the fcurves in the original action for fcurve in anim_data.nla_tracks[i].strips[0].action.fcurves: if fcurve_bones_path(obj, fcurve): continue for keyframe in fcurve.keyframe_points: if keyframe.co[0] not in old_keys: continue difference = old_keys[keyframe.co[0]] - keyframe.co[0] keyframe.co[0] = old_keys[keyframe.co[0]] if keyframe.interpolation == 'BEZIER': keyframe.handle_left[0] += difference keyframe.handle_right[0] += difference def view_all_keyframes(self, context): '''Creates new fcurves with the keyframes from the all the layers''' obj = self.id_data anim_data = anim_data_type(obj) nla_tracks = anim_data.nla_tracks #if animation layers is still not completly loaded then return if len(anim_data.nla_tracks) != len(obj.Anim_Layers) or anim_data.action is None: return #remove old Anim Layers fcurves tracknames = [track.name for track in nla_tracks] for track in (anim_data.nla_tracks): if len(track.strips) != 1: continue action = track.strips[0].action for i, trackname in enumerate(tracknames): fcu = action.fcurves.find(trackname, index=i) if not fcu: #remove all the fcurves/channels in the group and mark as removed continue if fcu.group.name != 'Anim Layers': continue fcu.group.lock = False for fcu_remove in fcu.group.channels: action.fcurves.remove(fcu_remove) #break if not self.view_all_keyframes: #If the option is uncheck then finish edit and return self.edit_all_keyframes = False return global fcu_layers fcu_layers = {} for i, track in enumerate(nla_tracks): if i == obj.als.layer_index or track.strips[0].action is None or not len(track.strips[0].action.fcurves) or obj.Anim_Layers[i].lock: continue #create a new fcurve with the name of the track fcu_layer = anim_data.action.fcurves.new(track.name, index=i, action_group='Anim Layers') fcu_layer.update() fcu_layer.is_valid = True frames = get_fcu_layer_keyframes(obj, context, track) if not len(frames): continue keyframes = np.repeat(frames, 2) # keyframes = np.zeros(len(frames)*2) # keyframes[::2] = frames #create new keyframes for all the stored keys keyframes_amount = int(len(keyframes)*0.5) fcu_layer.keyframe_points.add(keyframes_amount) fcu_layer.keyframe_points.foreach_set('co', keyframes) #fcu_layer.keyframe_points.foreach_set('interpolation', [0]*keyframes_amount) fcu_layer.keyframe_points.foreach_set('type', [int(self.view_all_type)]*keyframes_amount) # for key in keyframes: # fcu_layer.keyframe_points.add(1) # fcu_layer.keyframe_points[-1].co[0] = key # fcu_layer.keyframe_points[-1].co[1] = key # fcu_layer.keyframe_points[-1].interpolation = 'LINEAR' # fcu_layer.keyframe_points[-1].type = self.view_all_type fcu_layer.hide = True fcu_layer.update() #Make sure lock is turned off when selecting new layer and edit is turned on if fcu_layer is not None and self.edit_all_keyframes: fcu_layer.group.lock = False frames_keys = dict(zip(frames, fcu_layer.keyframe_points)) #store the fcurves and keyframes fcu_layers.update({fcu_layer.data_path : frames_keys}) unlock_edit_keyframes(self, context) def unlock_edit_keyframes(self, context): '''Lock or unlock the fcurves of the Multiple layers with the edit all keyframes property''' obj = self.id_data if not self.view_all_keyframes or obj is None: return anim_data = anim_data_type(obj) for i, layer in enumerate(obj.Anim_Layers): #look for the Anim Layers fcurve if layer.lock or anim_data.action is None or i == obj.als.layer_index: continue fcu = anim_data.action.fcurves.find(layer.name, index = i) if self.edit_all_keyframes: fcu.group.lock = False else: fcu.group.lock = True return ###################################################### PROPERTY FUNCTIONS ################################################ def collect_children_collections(obj_col, col_hide_viewlayer, col_hide_viewport, layer_collection, col_checked = []): '''part of unhide objects collections, iterate over all the children in the viewlayer collections''' #iterate over all the children collections = bpy.data.collections for col in layer_collection.children: if obj_col.name != col.name and obj_col not in collections[col.name].children_recursive: continue if col in col_checked: continue if col.hide_viewport: col.hide_viewport = False col_hide_viewlayer.append(col) if collections[col.name].hide_viewport: collections[col.name].hide_viewport = False col_hide_viewport.append(collections[col.name]) col_checked.append(col) #repeat the same function to iterate over the next children if len(col.children): col_hide_viewlayer, col_hide_viewport = collect_children_collections(obj_col, col_hide_viewlayer, col_hide_viewport, col, col_checked) return col_hide_viewlayer, col_hide_viewport def unhide_collections_on_load(anim_layer_objects): '''unhide objects and collections during anim layers load to avoid errors with the nla''' #list of hidden realted collections in the view layer col_hide_viewlayer = [] col_hide_viewport = [] #use this for unmuting the eye icon hidden_objs = [] #get all the collections related to the objects that have anim layers included obj_users_collection = [] for obj in anim_layer_objects: if obj is None: continue #list of hidden related collections if obj.hide_viewport: obj.hide_viewport = False col_hide_viewport.append(obj) if obj.hide_get(): hidden_objs.append(obj) obj.hide_set(False) obj_users_collection += [obj_col for obj_col in obj.users_collection] obj_users_collection = set(obj_users_collection) layer_collection = bpy.context.view_layer.layer_collection #get all the collections that influence the object for obj_col in obj_users_collection: col_hide_viewlayer, col_hide_viewport = collect_children_collections(obj_col, col_hide_viewlayer, col_hide_viewport, layer_collection) return col_hide_viewlayer, col_hide_viewport, hidden_objs def hide_collections_on_load(col_hide_viewlayer, col_hide_viewport, hidden_objs): #revert back hidden layers, so they are hidden again for col in col_hide_viewlayer: col.hide_viewport = True for col in col_hide_viewport: col.hide_viewport = True for obj in hidden_objs: obj.hide_set(True) def tweak_mode_objs(scene): #store objects that are in tweak mode tweak_mode = {} 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 anim_data = anim_data_type(obj) if anim_data is None: continue tweak_mode[anim_data] = anim_data.use_tweak_mode return tweak_mode def tweak_mode_upper_stack(context, obj, anim_data, enter = True): #override nla context, use a temporaray area #context = bpy.context #window = context.window_manager.windows[0] window = context.window screen = window.screen old_area = screen.areas[0].type screen.areas[0].type = 'NLA_EDITOR' area = screen.areas[0] scene = context.scene #record tweak mode of other objects tweak_mode_objects = tweak_mode_objs(scene) #obj = anim_data.id_data # error = False with context.temp_override(window=window, area=area): # tweak mode needs to be turned on on animation data to be able to go in the context #if anim_data.use_tweak_mode: if scene.is_nla_tweakmode: try: bpy.ops.nla.tweakmode_exit() except RuntimeError: anim_data.use_tweak_mode = False if anim_data.use_tweak_mode: anim_data.use_tweak_mode = False if enter: #making sure there is no active action outside the nla if anim_data.action: anim_data.action = None try: bpy.ops.nla.tweakmode_enter(use_upper_stack_evaluation=True) except RuntimeError as e: print(obj.name, e) #check again if unhiding helped if anim_data.use_tweak_mode: obj.als.upper_stack = True else: obj.als.upper_stack = False #reset tweak mode that it's not appearing in Lower stack anim_data.use_tweak_mode = True anim_data.use_tweak_mode = False #restore tweak mode from other objects for obj_anim_data, value in tweak_mode_objects.items(): if obj_anim_data == anim_data: continue obj_anim_data.use_tweak_mode = value screen.areas[0].type = old_area def update_layer_index(self, context): '''select the new action clip when there is a new selection in the ui list and make all the updates for this Layer''' obj = self.id_data if obj is None:# or context.object is None: return if not self.turn_on: return if not len(obj.Anim_Layers): return anim_data = anim_data_type(obj) for track in anim_data.nla_tracks: track.select = False if len(track.strips): track.strips[0].select = False nla_track = anim_data.nla_tracks[self.layer_index] if not len(nla_track.strips): anim_data.use_tweak_mode = False return strip = nla_track.strips[0] if strip.action is None: anim_data.use_tweak_mode = False return #select and activate the strip and track strip.select = True nla_track.select = True anim_data.nla_tracks.active = nla_track if obj.Anim_Layers[self.layer_index].lock: #anim_data.use_tweak_mode = False tweak_mode_upper_stack(context, obj, anim_data, enter = False) return if not obj.als.upper_stack: subscriptions.subscriptions_remove() tweak_mode_upper_stack(context, obj, anim_data) subscriptions.subscriptions_add(context.scene) else: anim_data.use_tweak_mode = False if not obj.Anim_Layers[self.layer_index].lock: anim_data.use_tweak_mode = True if obj.als.view_all_keyframes: obj.als.view_all_keyframes = True def layer_mute(self, context): obj = self.id_data index = list(obj.Anim_Layers).index(self) anim_data = anim_data_type(obj) anim_data.nla_tracks[index].mute = self.mute #Exclude muted layers from view all keyframes if obj.als.view_all_keyframes: obj.als.view_all_keyframes = True def layer_solo(self, context): obj = context.object anim_data = anim_data_type(obj) #added a skip boolean so that when layer.solo = False it doesnt iterate through all the layers because of the call, since only one layer can be solo global skip try: if skip: return except NameError: skip = False if self.solo: for i, layer in enumerate(obj.Anim_Layers): if layer != self: skip = True layer.solo = False anim_data.nla_tracks[i].mute = True else: anim_data.nla_tracks[i].mute = False skip = False else: #when turned off restore track mute from the layers mute property for i, track in enumerate(anim_data.nla_tracks): track.mute = obj.Anim_Layers[i].mute def layer_lock(self, context): obj = self.id_data index = list(obj.Anim_Layers).index(self) anim_data = anim_data_type(obj) nla_tracks = anim_data.nla_tracks if not self.lock: if len(nla_tracks[index].strips) != 1 or nla_tracks[index].strips[0].type == 'META': self.lock = True #Get out of tweak mode if index == obj.als.layer_index: obj.als.layer_index = obj.als.layer_index #Exclude locked layers from view all keyframes if obj.als.view_all_keyframes: obj.als.view_all_keyframes = True def only_selected_bones(self, context): '''assign selected bones to a global variable that will be checked in the handler''' view_all_keyframes(self, context) # if self.only_selected_bones: # # global selected_bones # # selected_bones = context.selected_pose_bones # view_all_keyframes(self, context) # else: # view_all_keyframes(self, context) # #del selected_bones def data_type_update(self, context): obj = self.id_data anim_data = anim_data_type(obj) if anim_data is None: obj.Anim_Layers.clear() return if not len(anim_data.nla_tracks): obj.Anim_Layers.clear() return obj.als.layer_index = 0 register_layers(obj, anim_data.nla_tracks) #change bake method if working with shapekeys if self.baketype == 'NLA' and self.data_type == 'KEY': self.baketype = 'AL' def layer_name_update(self, context): #if layer name exists then add a unique name obj = self.id_data if context.object is None: return layer_names = [layer.name for layer in context.object.Anim_Layers if layer != self] if self.name in layer_names: self.name = unique_name(layer_names, self.name) anim_data = anim_data_type(obj) if not hasattr(anim_data, 'nla_tracks'): return nla_tracks = anim_data.nla_tracks override_tracks = check_override_tracks(obj, anim_data) index = list(obj.Anim_Layers).index(self) track = nla_tracks[index] if not len(track.strips): return strip = track.strips[0] if self.name != track.name: #synchronize override_tracks if track.name in override_tracks: override_tracks[track.name].name = self.name track.name = self.name if len(track.strips) == 1: strip.name = self.name if strip.action is None: return if obj.als.auto_rename and strip.action.name != self.name: strip.action.name = self.name def influence_mute_update(self, context): '''added an extra property for the influence mute because it was disabled with override libraries''' obj = self.id_data if not len(obj.Anim_Layers): return index = obj.Anim_Layers.find(self.name) anim_data = anim_data_type(obj) if not len(anim_data.nla_tracks[index].strips): return strip = anim_data.nla_tracks[index].strips[0] if not len(strip.fcurves): return fcu = strip.fcurves[0] fcu.mute = self.influence_mute fcu.lock = self.influence_mute if self.influence_mute: self.influence = strip.influence def influence_update(self, context): obj = self.id_data if not len(obj.Anim_Layers): return index = obj.Anim_Layers.find(self.name) anim_data = anim_data_type(obj) if not len(anim_data.nla_tracks[index].strips): return strip = anim_data.nla_tracks[index].strips[0] strip.influence = self.influence strip.fcurves[0].update() def blend_type_values(self, obj, strip): '''Changing the values for scale and rotation_quaternion when switching between blend modes''' if obj.als.data_type != 'OBJECT': return if obj.animation_data.action is None: return if not len(obj.animation_data.action.fcurves): return for fcu in strip.action.fcurves: if 'scale' not in fcu.data_path and 'rotation_quaternion' not in fcu.data_path: continue default_value = bake_ops.attr_default(obj, (fcu.data_path, fcu.array_index))[fcu.array_index] #switching from replace to add layer, needs to reduce value of 1 from the scale and rotation_quaternion for keyframe in fcu.keyframe_points: if strip.blend_type == 'REPLACE' and (self.blend_type == 'ADD' or self.blend_type == 'SUBTRACT'): keyframe.co[1] -= default_value keyframe.handle_right[1] -= default_value keyframe.handle_left[1] -= default_value elif (strip.blend_type == 'ADD' or strip.blend_type == 'SUBTRACT') and self.blend_type == 'REPLACE': keyframe.co[1] += default_value keyframe.handle_right[1] += default_value keyframe.handle_left[1] += default_value def blend_type_update(self, context): '''synchronize the blend property with the NLA Blend''' obj = self.id_data anim_data = anim_data_type(obj) strip = anim_data.nla_tracks[obj.als.layer_index].strips[0] if self.blend_type == strip.blend_type: return if obj.als.auto_blend: blend_type_values(self, obj, strip) strip.blend_type = self.blend_type def auto_rename(self, context): '''Use auto rename when Turning it on''' if not self.auto_rename: return obj = self.id_data if obj is None: return anim_data = anim_data_type(obj) if anim_data is None: return if not len(anim_data.nla_tracks): return if anim_data.action is None: return name = anim_data.action.name obj.Anim_Layers[obj.als.layer_index].name = name anim_data.nla_tracks[obj.als.layer_index].name = name anim_data.nla_tracks[obj.als.layer_index].strips[0].name = name def auto_rename_default(obj): '''Apply the default auto renaming from the addon preferences''' folder_name = addon_folder_path() obj.als.auto_rename = bpy.context.preferences.addons[folder_name].preferences.auto_rename def addon_folder_path(): folder_path = os.path.dirname(os.path.realpath(__file__)) folder_name = os.path.basename(folder_path) return folder_name def add_inbetween_key(self, context): '''Adding a Breakdown keyframe that works also in Layers''' obj = self.id_data anim_data = anim_data_type(obj) strip = anim_data.nla_tracks[obj.als.layer_index].strips[0] frame = round(bake_ops.frame_evaluation(context.scene.frame_current, strip), 3) for fcu in anim_data.action.fcurves: #filter selected bones if obj.mode == 'POSE': #apply only to selected bones if obj.als.only_selected_bones: bones = [bone.path_from_id() for bone in context.selected_pose_bones] if fcu.data_path.split('].')[0]+']' not in bones: continue if not multikey.filter_properties(obj, fcu): continue #get the last previous key for keyframe in fcu.keyframe_points: if round(keyframe.co[0], 3) > frame: key_after = keyframe break elif round(keyframe.co[0], 3) < frame: key_before = keyframe else: key_added = keyframe if 'key_after' not in locals() or 'key_before' not in locals(): continue if 'key_added' not in locals(): fcu.keyframe_points.add(1) key_added = fcu.keyframe_points[-1] value = key_before.co[1] + (key_after.co[1] - key_before.co[1]) * self.inbetweener key_added.co = (frame, value) fcu.update() del key_after del key_before del key_added self['inbetweener'] = 0.5 def load_action(self, context): '''Load a new action from the layer list''' obj = self.id_data index = obj.Anim_Layers.find(self.name) anim_data = anim_data_type(obj) # if self.lock: # return if self.action == 'None': return track = anim_data.nla_tracks[index] action = self.action if not len(track.strips): strip = track.strips.new(name = track.name, start=0, action = action) subscriptions.frameend_update_callback() strip.use_sync_length = False use_animated_influence(strip) return subscriptions.subscriptions_remove() strip = track.strips[0] #action = bpy.data.actions[self.action] if index != obj.als.layer_index: return if strip.action == action: return tweak_mode_upper_stack(context, obj, anim_data, enter = False) strip.action = action if action is None: return if obj.als.auto_blend and len(action.fcurves): strip.blend_type = auto_blendtype(obj, action, strip.blend_type) #Auto rename if obj.als.auto_rename: obj.Anim_Layers[index].name = action.name track.name = action.name strip.name = action.name if self.lock: return obj.als.view_all_keyframes = obj.als.view_all_keyframes #anim_data.use_nla = True tweak_mode_upper_stack(context, obj, anim_data, enter = True) subscriptions.subscriptions_add(context.scene) def auto_blendtype(obj, action, current_blend): '''apply blend type automatically''' if action is None: return if not len(action.fcurves): return current_blend count = 0 for fcu in action.fcurves: if not 'scale' in fcu.data_path and not 'rotation_quaternion' in fcu.data_path: continue default_value = bake_ops.attr_default(obj, (fcu.data_path, fcu.array_index))[fcu.array_index] if not default_value: continue count += 1 for keyframe in fcu.keyframe_points: if keyframe.co[1] == 0: return 'ADD' if count: return 'REPLACE' else: return current_blend def layer_frame_start(self, context): '''synchronize action start and strip start''' if not self.frame_range: return if self.frame_start > self.frame_end: self.frame_end = self.frame_start obj = self.id_data index = obj.Anim_Layers.find(self.name) anim_data = anim_data_type(obj) if not len(anim_data.nla_tracks[index].strips): return strip = anim_data.nla_tracks[index].strips[0] strip.frame_start = self.frame_start if strip.repeat <= 1: strip_action_recalc(self, strip) else: self['offset'] = round((strip.frame_start - strip.action_frame_start), 3) recalculate_repeat(self, strip) def recalculate_repeat(self, strip): '''get the repeat value from the frame range''' if self.repeat <= 1: return action_frame_range = strip.action_frame_end - strip.action_frame_start strip_frame_range = self.frame_end - self.frame_start strip.repeat = strip_frame_range / (action_frame_range * strip.scale) # strip.repeat = strip_frame_range / action_frame_range self ['repeat'] = strip.repeat def layer_frame_end(self, context): '''synchronize action end and strip end''' if not self.frame_range: return if self.frame_end < self.frame_start: self.frame_start = self.frame_end obj = self.id_data index = obj.Anim_Layers.find(self.name) anim_data = anim_data_type(obj) if not len(anim_data.nla_tracks[index].strips): return strip = anim_data.nla_tracks[index].strips[0] if strip.repeat <= 1: strip_action_recalc(self, strip) else: strip.frame_end = self.frame_end strip.action_frame_end = strip.action.frame_range[1] recalculate_repeat(self, strip) def layer_frame_range(self, context): '''update the custom frame range when turned on and off''' obj = self.id_data anim_data = anim_data_type(obj) index = obj.Anim_Layers.find(self.name) strip = anim_data.nla_tracks[index].strips[0] if not self.frame_range: # self['repeat'] = strip.repeat strip.repeat = 1 #change strip repeat but keep self.repeat value stored strip.use_reverse = False subscriptions.frameend_update_callback() layer_offset(self, context) return if len(anim_data.nla_tracks) != len(obj.Anim_Layers): return if self.repeat != 1: strip.repeat = self.repeat if self.frame_end: #if there is a frame end defined restore previous settings strip.scale = self.speed strip.repeat = self.repeat strip.frame_start = self.frame_start strip.frame_end = self.frame_end action_start = strip.action.frame_range[0] start_offset = action_start - strip.frame_start strip.action_frame_end = strip.frame_end + start_offset - (start_offset * 1/strip.scale) - self.offset * (1/strip.scale) strip.frame_end = self.frame_end layer_offset(self, context) layer_repeat(self, context) else: if not len(anim_data.nla_tracks[index].strips): return self.frame_end = anim_data.nla_tracks[index].strips[0].frame_end self.frame_start = anim_data.nla_tracks[index].strips[0].frame_start strip.extrapolation = 'NOTHING' def layer_repeat(self, context): '''Multiply the action speed but keep strip limits the same''' obj = self.id_data index = obj.Anim_Layers.find(self.name) anim_data = anim_data_type(obj) if not len(anim_data.nla_tracks[index].strips): return strip = anim_data.nla_tracks[index].strips[0] if strip.repeat == self.repeat == 1: return strip.action_frame_start = strip.action.frame_range[0]#- self.offset strip.action_frame_end = strip.action.frame_range[1]#- self.offset action_range = (strip.action.frame_range[1] - strip.action.frame_range[0]) strip.repeat = self.repeat strip.frame_start = self['frame_start'] = strip.action.frame_range[0] + self.offset strip.frame_end = self['frame_end'] =strip.action.frame_range[0] + (action_range * strip.repeat* strip.scale) + self.offset def layer_speed(self, context): '''Multiply the action speed but keep strip limits the same''' obj = self.id_data index = obj.Anim_Layers.find(self.name) anim_data = anim_data_type(obj) if not len(anim_data.nla_tracks[index].strips): return strip = anim_data.nla_tracks[index].strips[0] frame_end = strip.frame_end frame_start = strip.frame_start strip.scale = self.speed if not self.frame_range: action_start = strip.action.frame_range[0] start_offset = action_start - strip.frame_start strip.action_frame_start = frame_start + start_offset - (start_offset * 1/strip.scale) - self.offset * (1/strip.scale) strip.action_frame_end = frame_end + start_offset - (start_offset * 1/strip.scale) - self.offset * (1/strip.scale) strip.frame_end = frame_end return if strip.repeat <= 1: strip_action_recalc(self, strip) else: recalculate_repeat(self, strip) if strip.use_sync_length: sync_frame_range(context) def layer_offset(self, context): '''Offset the action keyframes but keep strip limits the same''' obj = self.id_data index = obj.Anim_Layers.find(self.name) anim_data = anim_data_type(obj) if not len(anim_data.nla_tracks[index].strips): return strip = anim_data.nla_tracks[index].strips[0] #changing only action frame start when custom frame range turned off if not self.frame_range: frame_end = strip.frame_end frame_start = strip.frame_start action_start = strip.action.frame_range[0] start_offset = action_start - strip.frame_start strip.action_frame_start = frame_start + start_offset - (start_offset * 1/strip.scale) - self.offset * (1/strip.scale) strip.action_frame_end = frame_end + start_offset - (start_offset * 1/strip.scale) - self.offset * (1/strip.scale) strip.frame_end = frame_end return if strip.repeat <= 1: strip_action_recalc(self, strip) else: action_range = strip.action_frame_end - strip.action_frame_start strip.frame_start = strip.action.frame_range[0] + self.offset strip.frame_end = strip.action_frame_start + action_range * strip.repeat * strip.scale + self.offset self['frame_start'] = strip.frame_start self['frame_end'] = strip.frame_end if strip.use_sync_length: sync_frame_range(context) def strip_action_recalc(self, strip): strip.scale = self.speed strip.repeat = self.repeat action_start = strip.action.frame_range[0] start_offset = action_start - strip.frame_start strip.action_frame_start = self.frame_start + start_offset - (start_offset * 1/strip.scale) - self.offset * (1/strip.scale) strip.action_frame_end = self.frame_end + start_offset - (start_offset * 1/strip.scale) - self.offset * (1/strip.scale) strip.frame_start = self.frame_start strip.frame_end = self.frame_end ###################################################### HELPER FUNCTIONS ################################################ def redraw_areas(areas): for area in bpy.context.window_manager.windows[0].screen.areas: if area.type in areas: area.tag_redraw() def anim_data_type(obj, toggle = False): if obj.als.data_type == 'OBJECT' and not toggle: if not hasattr(obj, 'animation_data'): return None anim_data = obj.animation_data else: if not hasattr(obj.data.shape_keys, 'animation_data'): return None anim_data = obj.data.shape_keys.animation_data return anim_data def anim_datas_append(obj): '''append shapekey animation data if it also exists''' anim_datas = [obj.animation_data] if hasattr(obj.data, 'shape_keys'): if hasattr(obj.data.shape_keys, 'animation_data'): #anim_datas = {obj.animation_data, obj.data.shape_keys.animation_data} anim_datas.append(obj.data.shape_keys.animation_data) return anim_datas def unique_name(collection, name): '''add numbers to tracks if they have the same name''' if name not in collection: return name nr = 1 if '.' in name: end = name.split('.')[-1] if end.isnumeric(): nr = int(end) name = '.'.join(name.split('.')[:-1]) while name + '.' + str(nr).zfill(3) in collection: nr += 1 return name + '.' + str(nr).zfill(3) #checks if the object has an action and if it exists in the NLA def action_search(action, nla_tracks): '''returns True if action already exists in the nla_tracks''' if action is None: return False for track in nla_tracks: for strip in track.strips: if strip.action == action: return True return False def select_layer_bones(self, context): obj = context.object strips = obj.animation_data.nla_tracks[obj.als.layer_index].strips if len(strips) != 1 or strips[0].action is None: return for fcu in strips[0].action.fcurves: if 'pose.bones' in fcu.data_path: bone = fcu.data_path.split('"')[1] if bone in obj.data.bones: obj.data.bones[bone].select = True ###################################################### CLASSES ########################################################### class SelectBonesInLayer(bpy.types.Operator): """Select bones with keyframes in the current layer""" bl_idname = "anim.bones_in_layer" bl_label = "Select layer bones" bl_icon = "BONE_DATA" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.object and context.object.mode == 'POSE' def execute(self, context): select_layer_bones(self, context) return {'FINISHED'} class OverrideError(bpy.types.Operator): bl_idname = "message.layersoverride" bl_label = "WARNING!" bl_icon = "ERROR" confirm: bpy.props.BoolProperty(default=False) def execute(self, context): if not self.confirm: return {'FINISHED'} #use bpy.path for the absolute path filepath = bpy.path.abspath(context.object.override_library.reference.library.filepath) blenderpath = bpy.app.binary_path import subprocess subprocess.Popen([blenderpath, filepath]) return {'FINISHED'} def invoke(self, context, event): wm = context.window_manager obj_name = context.object.name return wm.invoke_props_dialog(self, width=450+len(obj_name)) def draw(self, context): layout = self.layout row = layout.row() obj_name = context.object.name row = layout.row() row.label(text="The object %s is an override library that uses layers or tracks in its source file."%obj_name) row = layout.row() row.label(text="Please clean the source file from animation layers or nla tracks.") layout.separator(factor = 2) row = layout.row() row.alignment = 'LEFT' row.prop(self, "confirm", text="Open the source file in a new Blender window") class ClearNLA(bpy.types.Operator): bl_idname = "anim.clear_nla_warning" bl_label = "WARNING!" bl_icon = "ERROR" confirm: bpy.props.BoolProperty(default=True) def execute(self, context): # def draw_error(self, context): # self.layout.label(text = 'Override NLA tracks are found and are not removable. Remove them inside the referenced file') obj = context.object anim_datas = anim_datas_append(obj) for anim_data in anim_datas: if anim_data is None: continue if self.confirm: #start to delete only after the library override referenced tracks nla_tracks = anim_data.nla_tracks if not len(nla_tracks): continue clear_nla_tracks(obj, anim_data) if check_override_tracks(obj, anim_data) or check_override_layers(obj): bpy.ops.message.layersoverride('INVOKE_DEFAULT') continue obj.Anim_Layers.clear() subscriptions.subscriptions_remove() obj.als['turn_on'] = True start_animlayers(obj) subscriptions.subscriptions_add(context.scene) return {'FINISHED'} def invoke(self, context, event): wm = context.window_manager return wm.invoke_props_dialog(self, width=525) def draw(self, context): layout = self.layout col = layout.column() obj_name = context.object.name col.label(text=obj_name+" has already tracks in the NLA editor, which have been created before using animation layers.") row = col.row() row.alignment = 'CENTER' row.prop(self, "confirm", text="Remove NLA tracks") def clear_nla_tracks(obj, anim_data): '''remove all the nla tracks''' nla_tracks = anim_data.nla_tracks override_tracks = check_override_tracks(obj, anim_data) anim_data.use_tweak_mode = False for track in nla_tracks: track.is_solo = False if track.name not in override_tracks: anim_data.nla_tracks.remove(track) class ClearActiveAction(bpy.types.Operator): bl_idname = "anim.clear_active_action_warning" bl_label = "WARNING!" bl_icon = "ERROR" # 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), # ] # ) def execute(self, context): self.proceed = context.preferences.addons[__package__].preferences.proceed obj = context.object anim_datas = anim_datas_append(obj) for anim_data in anim_datas: if anim_data is None: continue if self.proceed == 'REMOVE_LAYERS': #start to delete only after the library override referenced tracks if not len(anim_data.nla_tracks): continue obj.als.layer_index = 0 clear_nla_tracks(obj, anim_data) if check_override_tracks(obj, anim_data) or check_override_layers(obj): bpy.ops.message.layersoverride('INVOKE_DEFAULT') continue obj.Anim_Layers.clear() elif self.proceed == 'REMOVE_ACTION': anim_data.action = None elif self.proceed == 'ADD_ACTION': action = anim_data.action # anim_data.action = None index = len(obj.Anim_Layers) - 1 obj.als.layer_index = index #add_animlayer(layer_name = action.name , duplicate = False, index = 1, blend_type = 'COMBINE') add_animlayer(unique_name(obj.Anim_Layers, action.name), index = index, blend_type = 'REPLACE') obj.als.layer_index += 1 # new_track.strips[0].action = action subscriptions.subscriptions_remove() obj.als['turn_on'] = True start_animlayers(obj) subscriptions.subscriptions_add(context.scene) return {'FINISHED'} def invoke(self, context, event): wm = context.window_manager return wm.invoke_props_dialog(self, width = 450) def draw(self, context): layout = self.layout row = layout.row(align = True) obj_name = context.object.name row.alignment = 'CENTER' row.label(text=obj_name+" already has older layers and an active action that are not matching") # row = col.row() #col.alignment = 'CENTER' split = layout.split(factor = 0.25) split.label(text = 'How to Proceed') split.prop(context.preferences.addons[__package__].preferences, "proceed", text ='') def update_action_list(scene): '''update all the objects layers with the updated action item list when a new layer was added''' for AL_object in scene.AL_objects: obj = AL_object.object if obj is None: continue anim_data = anim_data_type(obj) i = obj.als.layer_index if len(anim_data.nla_tracks[i].strips) != 1: return obj.Anim_Layers.update() layer = obj.Anim_Layers[0] layer.action = anim_data.nla_tracks[0].strips[0].action def add_animlayer(layer_name = 'Anim_Layer' , duplicate = False, index = 1, blend_type = 'COMBINE'): '''Add an animation layer''' obj = bpy.context.object check_overrides_ALobjects(obj) anim_data = anim_data_type(obj) action = anim_data.action nla_tracks = anim_data.nla_tracks if obj.als.layer_index < 0 : obj.als['layer_index'] = 0 previous = None if index == 0 else nla_tracks[obj.als.layer_index] new_track = nla_tracks.new(prev = previous) new_track.name = layer_name new_track.lock = True #if there is no action to duplicate then cancel duplication and create new layer if len(obj.Anim_Layers): if obj.Anim_Layers[obj.als.layer_index].action is None: duplicate = False #check if the object already has an action and if it exists in the NLA, if not create a new one if action is None or (action_search(action, anim_data.nla_tracks) and not duplicate): # action = bpy.data.actions.new(name=new_track.name) action.id_root = obj.als.data_type #update_action_list(bpy.context.scene) elif duplicate: action = obj.Anim_Layers[obj.als.layer_index].action else: action = action #strip settings new_strip = new_track.strips.new(name = new_track.name,start=0, action = action) if duplicate: new_strip.action_frame_start = anim_data.nla_tracks[obj.als.layer_index].strips[0].action_frame_start new_strip.action_frame_end = anim_data.nla_tracks[obj.als.layer_index].strips[0].action_frame_end else: new_strip.action_frame_start = 0 visible_layers(obj, anim_data.nla_tracks) subscriptions.frameend_update_callback() #subscriptions.frameend_update_callback(bpy.context.scene) #auto_rename(obj.als, bpy.context) new_strip.blend_type = blend_type new_strip.use_sync_length = False use_animated_influence(new_strip) return new_track #adding a new track, action and strip class AddAnimLayer(bpy.types.Operator): """Add animation layer""" bl_idname = "anim.add_anim_layer" bl_label = "Add Animation Layer" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): obj = context.object #subscriptions.subscriptions_remove() anim_data = anim_data_type(obj) # addon_name = addon_folder_path() blend_type = context.preferences.addons[__package__].preferences.blend_type if subscriptions.check_handler in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.remove(subscriptions.check_handler) if obj.als.data_type == 'OBJECT': layer_name = 'Anim_Layer' base_name = 'Base_Layer' if anim_data is None: anim_data = obj.animation_data_create() elif obj.als.data_type == 'KEY': if not obj.data.shape_keys: obj.shape_key_add(name = 'Basis') layer_name = 'Shapekeys_Layer' base_name = 'Base_Shapekeys' if anim_data is None: anim_data = obj.data.shape_keys.animation_data_create() nla_tracks = anim_data.nla_tracks if not len(nla_tracks): #starting animation layers and getting the default sync layer names obj.als.auto_rename = context.preferences.addons[__package__].preferences.auto_rename add_animlayer(base_name, index = 0, blend_type = blend_type) #using a temporary variable instead of calling update_track_list all the time with obj.als.layer_index index = 0 if anim_data.action: add_animlayer(layer_name, blend_type = blend_type) index += 1 anim_data.action.use_fake_user = True anim_data.action = None else: add_animlayer(unique_name(obj.Anim_Layers, layer_name), blend_type = blend_type) index = obj.als.layer_index + 1 #register_layers(obj, nla_tracks) override_tracks = check_override_tracks(obj, anim_data) if override_tracks: bpy.ops.message.layersoverride('INVOKE_DEFAULT') #if override tracks exist then make sure selection is on top of them while anim_data.nla_tracks[index].name in override_tracks: index += 1 obj.als.layer_index = index subscriptions.animlayers_frame(self, context) #subscriptions.subscriptions_add(context.scene) if subscriptions.check_handler not in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.append(subscriptions.check_handler) return {'FINISHED'} class DuplicateAnimLayer(bpy.types.Operator): """Duplicate animation layer""" bl_idname = "anim.duplicate_anim_layer" bl_label = "Duplicate Animation Layer" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): if subscriptions.check_handler in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.remove(subscriptions.check_handler) obj = context.object anim_data = anim_data_type(obj) nla_tracks = anim_data.nla_tracks i = obj.als.layer_index blend = nla_tracks[i].strips[0].blend_type track_name = nla_tracks[i].name name = unique_name(obj.Anim_Layers, track_name) new_track = add_animlayer(layer_name = name, duplicate = True, blend_type = blend) new_strip = new_track.strips[0] action = new_strip.action if obj.als.linked == False: tweak_mode_upper_stack(context, obj, anim_data, enter = False) new_action = action.copy() new_strip.action = new_action #duplicate custom frame range properties register_layers(obj, nla_tracks) # for prop in obj.Anim_Layers[i].bl_rna.properties.keys(): # if prop not in obj.Anim_Layers[i+1].bl_rna.properties.keys(): # continue # if prop in ['name', 'action', 'solo'] or obj.Anim_Layers[i].bl_rna.properties[prop].is_readonly or obj.Anim_Layers[i].bl_rna.properties[prop].is_hidden: # continue # #obj.Anim_Layers[i+1][prop] = obj.Anim_Layers[i][prop] # setattr(obj.Anim_Layers[i+1], prop, obj.Anim_Layers[i][prop]) obj.Anim_Layers[i+1].frame_range = obj.Anim_Layers[i].frame_range obj.Anim_Layers[i+1].repeat = obj.Anim_Layers[i].repeat obj.Anim_Layers[i+1].frame_start = obj.Anim_Layers[i].frame_start obj.Anim_Layers[i+1].frame_end = obj.Anim_Layers[i].frame_end obj.Anim_Layers[i+1].speed = obj.Anim_Layers[i].speed obj.Anim_Layers[i+1].offset = obj.Anim_Layers[i].offset obj.Anim_Layers[i+1].mute = obj.Anim_Layers[i].mute new_strip.use_reverse = nla_tracks[i].strips[0].use_reverse new_strip.use_sync_length = nla_tracks[i].strips[0].use_sync_length new_strip.extrapolation = nla_tracks[i].strips[0].extrapolation obj.als.layer_index += 1 #Turn on frame range if it was duplicated tweak_mode_upper_stack(context, obj, anim_data) if subscriptions.check_handler not in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.append(subscriptions.check_handler) return {'FINISHED'} class ExtractSelection(bpy.types.Operator): """Extract selected bones to a new Layer""" bl_idname = "anim.extract_selected_bones" bl_label = "Extract Selected Bones" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): obj = context.object return obj and obj.type == 'ARMATURE' and obj.mode == 'POSE' def execute(self, context): if subscriptions.check_handler in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.remove(subscriptions.check_handler) obj = context.object anim_data = anim_data_type(obj) nla_tracks = anim_data.nla_tracks blend = nla_tracks[obj.als.layer_index].strips[0].blend_type track_name = nla_tracks[obj.als.layer_index].name name = unique_name(obj.Anim_Layers, track_name + ' Extract') new_track = add_animlayer(layer_name = name, duplicate = True, blend_type = blend) bones_path = [bone.path_from_id() for bone in context.selected_pose_bones] bone_names = [bone.name for bone in context.selected_pose_bones] action = new_track.strips[0].action #create a new copy of the action new_action = action.copy() tweak_mode_upper_stack(context, obj, anim_data, enter = False) new_track.strips[0].action = new_action #remove fcurves of the selected bones in the original layer for fcu in action.fcurves: group = fcu.group.name if fcu.group is not None else None if fcu.data_path.split(']')[0]+']' in bones_path or group in bone_names: action.fcurves.remove(fcu) action.fcurves.update() #remove all bones that are not selected from the new extracted layer for fcu in new_action.fcurves: group = fcu.group.name if fcu.group is not None else None if fcu.data_path.split(']')[0]+']' not in bones_path and group not in bone_names: new_action.fcurves.remove(fcu) new_action.fcurves.update() register_layers(obj, nla_tracks) obj.als.layer_index += 1 tweak_mode_upper_stack(context, obj, anim_data) if subscriptions.check_handler not in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.append(subscriptions.check_handler) return {'FINISHED'} class ExtractMarkers(bpy.types.Operator): """Extract keyframes from Markers. Usefull for mocap cleanup""" bl_idname = "anim.extract_markers" bl_label = "Extract Marked keyframes" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return len(context.scene.timeline_markers) def execute(self, context): if subscriptions.check_handler in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.remove(subscriptions.check_handler) obj = context.object anim_data = anim_data_type(obj) nla_tracks = anim_data.nla_tracks blend = nla_tracks[obj.als.layer_index].strips[0].blend_type track_name = nla_tracks[obj.als.layer_index].name name = unique_name(obj.Anim_Layers, track_name + ' Extract') new_track = add_animlayer(layer_name = name, duplicate = True, blend_type = blend) if obj.type == 'ARMATURE': bones_path = [bone.path_from_id() for bone in context.selected_pose_bones] bone_names = [bone.name for bone in context.selected_pose_bones] tweak_mode_upper_stack(context, obj, anim_data, enter = False) action = new_track.strips[0].action #create a new copy of the action new_action = action.copy() new_track.strips[0].action = new_action markers = context.scene.timeline_markers marked_frames = [marker.frame for marker in markers] current_frame = context.scene.frame_current #remove all bones that are not selected from the new extracted layer for fcu in new_action.fcurves: if obj.type == 'ARMATURE': group = fcu.group.name if fcu.group is not None else None if fcu.data_path.split(']')[0]+']' not in bones_path and group not in bone_names: new_action.fcurves.remove(fcu) continue keyframes = fcu.keyframe_points #check the difference between the frames and the marked ones frames = np.zeros(len(keyframes)*2) keyframes.foreach_get('co', frames) missing_frames = set(marked_frames) - set(frames[::2]) #add the missing keyframes for frame in missing_frames: value = fcu.evaluate(frame) keyframes.insert(frame, value) #Create a duplicate of all the keyframes roundframes = [] smartkeys = [] for keyframe in keyframes: round_keyframe = round(keyframe.co[0]) if round_keyframe in marked_frames and round_keyframe not in roundframes: smartkey = bake_ops.smartkey(keyframe) smartkeys.append(smartkey) roundframes.append(round_keyframe) smartkeys = bake_ops.add_inbetween(smartkeys) for smartkey in smartkeys: smartkey.value = fcu.evaluate(smartkey.frame) smartkey.interpolation = 'BEZIER' i = 0 roundframes = [] while i < len(keyframes): round_keyframe = round(keyframes[i].co[0]) if keyframes[i].co[0] not in marked_frames:# or round_keyframe in roundframes: # print(f'fcu {fcu.data_path} removing keyframe {keyframes[i].co[0]}') keyframes.remove(keyframes[i]) else: keyframes[i].interpolation = 'BEZIER' # print(f'assign interpolation {round_keyframe}') roundframes.append(round_keyframe) i += 1 bake_ops.add_interpolations(fcu, smartkeys) # context.scene.frame_set(current_frame) new_action.fcurves.update() register_layers(obj, nla_tracks) obj.als.layer_index += 1 tweak_mode_upper_stack(context, obj, anim_data) if subscriptions.check_handler not in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.append(subscriptions.check_handler) return {'FINISHED'} class RemoveAnimLayer(bpy.types.Operator): """Remove animation layer""" bl_idname = "anim.remove_anim_layer" bl_label = "Remove Animation Layer" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): anim_data = anim_data_type(context.object) if context.object else None if hasattr(anim_data, 'nla_tracks'): return len(anim_data.nla_tracks) def execute(self, context): obj = context.object anim_data = anim_data_type(obj) nla_tracks = anim_data.nla_tracks track = nla_tracks[obj.als.layer_index] override_tracks = check_override_tracks(obj, anim_data) if track.name in override_tracks: return {'CANCELLED'} try: obj.Anim_Layers.remove(obj.als.layer_index) except TypeError: #library overrides currently can not remove items return {'CANCELLED'} if len(nla_tracks) == 1: tweak_mode_upper_stack(context, obj, anim_data, enter = False) nla_tracks.remove(track) #update the ui list item's index if obj.als.layer_index != 0: obj.als.layer_index -= 1 else: obj.als.layer_index = 0 return {'FINISHED'} def share_layerkeys_items(self, context): '''create the layer items for the share keys excluding the current layer''' obj = self.id_data return [(layer.name, layer.name, layer.name) for layer in obj.Anim_Layers if layer != obj.Anim_Layers[obj.als.layer_index]] class ShareLayerKeys(bpy.types.Operator): '''Share keyframes positions between layers''' bl_idname = "anim.share_layer_keys" bl_label = "Share Layer Keyframes" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return len(context.object.Anim_Layers) def execute (self, context): obj = context.object anim_data = anim_data_type(obj) fcu_frames = dict() current_strip = anim_data.nla_tracks[obj.als.layer_index].strips[0] #get the layer from the enumarator layer = obj.als.share_layer_keys if len(anim_data.nla_tracks[layer].strips) != 1: return {'CANCELLED'} action = anim_data.nla_tracks[layer].strips[0].action #store fcurves data path and array in a dictionary with all the frames for fcu in action.fcurves: if fcurve_bones_path(obj, fcu): continue #get all the keyframes keyframes = np.zeros([len(fcu.keyframe_points)*2]) fcu.keyframe_points.foreach_get('co', keyframes) only_frames = keyframes[::2] #Store all the fcurve data in a dictionary fcu_frames.update({(fcu.data_path, fcu.array_index) : (only_frames, fcu.group)}) #iterate over the stored fcurves and frames for fcu_key, data in fcu_frames.items(): frames, group = data value = None fcu = anim_data.action.fcurves.find(data_path = fcu_key[0], index = fcu_key[1]) if fcu is None: #if the fcurve doesn't exist then create it and assign the default value fcu = anim_data.action.fcurves.new(data_path = fcu_key[0], index = fcu_key[1]) value = bake_ops.attr_default(obj, fcu_key)[fcu_key[1]] if current_strip.blend_type in ['REPLACE', 'COMBINE'] else 0 #if the group doesn't exist in the current layer then create one and assign it if fcu.group is None: if group.name in anim_data.action.groups: new_group = anim_data.action.groups[group.name] else: new_group = anim_data.action.groups.new(group.name) fcu.group = new_group #exclude the frames that already exist in the current layer action fcurve if len(fcu.keyframe_points): keyframes = np.zeros([len(fcu.keyframe_points)*2]) fcu.keyframe_points.foreach_get('co', keyframes) only_frames = set(keyframes[::2]) frames = set(frames).difference(only_frames) #add all the keyframes for frame in frames: fcu.keyframe_points.add(1) #if there is no default value then get the value fromt the curve if value is None: value = fcu.evaluate(frame) fcu.keyframe_points[-1].co = (frame, value) fcu.update() return{'FINISHED'} def move_layer(dir, context): window = context.window screen = context.screen #Storing the first area in the screen old_area = screen.areas[0].type area = screen.areas[0] area.type = 'NLA_EDITOR' region = area.regions[1] obj = context.object anim_data = anim_data_type(obj) #exit global tweakmode if context.scene.is_nla_tweakmode: tweak_mode_upper_stack(context, obj, anim_data, enter = False) #deselect all track strips for obj in context.scene.objects: anim_data = anim_data_type(obj) if anim_data is None: continue if not hasattr(anim_data, 'nla_tracks'): continue for track in anim_data.nla_tracks: track.select = False #select only the current track strip obj = context.object anim_data = anim_data_type(obj) anim_data.nla_tracks[obj.als.layer_index].select = True with context.temp_override(window=window, area=area, region=region): bpy.ops.anim.channels_expand() bpy.ops.anim.channels_move(direction=dir) #restoring the old area screen.areas[0].type = old_area visible_layers(obj, anim_data.nla_tracks) class MoveAnimLayerUp(bpy.types.Operator): """Move the selected layer up""" bl_idname = "anim.layer_move_up" bl_label = "Move selected Animation layer up" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): obj = context.object anim_data = anim_data_type(obj) if obj else None if hasattr(anim_data, 'nla_tracks'): return len(anim_data.nla_tracks) > 1 def execute(self, context): obj = context.object index = obj.als.layer_index if index >= len(obj.animation_data.nla_tracks)-1: return {'CANCELLED'} subscriptions.subscriptions_remove() obj.Anim_Layers.move(index, index + 1) move_layer('UP', context) obj.als.layer_index += 1 subscriptions.subscriptions_add(context.scene) return {'FINISHED'} class MoveAnimLayerDown(bpy.types.Operator): """Move the selected layer down""" bl_idname = "anim.layer_move_down" bl_label = "Move selected Animation layer down" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): anim_data = anim_data_type(context.object) if context.object else None if hasattr(anim_data, 'nla_tracks'): return len(anim_data.nla_tracks) > 1 def execute(self, context): obj = context.object index = obj.als.layer_index if index == 0: return {'CANCELLED'} subscriptions.subscriptions_remove() obj.Anim_Layers.move(index, index -1) move_layer('DOWN', context) obj.als.layer_index -= 1 subscriptions.subscriptions_add(context.scene) return {'FINISHED'} def copy_modifiers(modifier, mod_list): attr = {} for key in dir(modifier): #add all the attributes into a dictionary value = getattr(modifier, key) attr.update({key: value}) mod_list.append(attr) return mod_list def paste_modifiers(fcu, mod_list): for mod in mod_list: if mod['type'] == 'CYCLES' and len(fcu.modifiers): #can add cycle modifier only as the first modifier continue new_mod = fcu.modifiers.new(mod['type']) if new_mod is None: continue for attr, value in mod.items(): if type(value) is float or type(value) is int or type(value) is bool: if not new_mod.is_property_readonly(attr): setattr(new_mod, attr, value) class CyclicFcurves(bpy.types.Operator): """Apply Cyclic Fcurve modifiers to all the selected bones and objects""" bl_idname = "anim.layer_cyclic_fcurves" bl_label = "Cyclic_Fcurves" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): if not context.object: return False anim_data = anim_data_type(context.object) if not hasattr(anim_data, 'action'): return False return anim_data.action is not None def execute(self, context): transform_types = ['location', 'rotation_euler', 'rotation_quaternion', 'scale'] for obj in context.selected_objects: anim_data = anim_data_type(obj) for fcu in anim_data.action.fcurves: if obj.mode == 'POSE': #apply only to selected bones if obj.als.only_selected_bones: bones = [bone.path_from_id() for bone in context.selected_pose_bones] if fcu.data_path.split('].')[0]+']' not in bones: continue if fcu.data_path in transform_types: continue else: if fcu.data_path not in transform_types and obj.als.data_type != 'KEY': continue if not multikey.filter_properties(obj, fcu): continue cycle_mod = False mod_list = [] if len(fcu.modifiers): #i = 0 while len(fcu.modifiers): modifier = fcu.modifiers[0] if modifier.type == 'CYCLES': modifier.mute = False cycle_mod = True break else: #if its a different modifier then store and remove it mod_list = copy_modifiers(modifier, mod_list) fcu.modifiers.remove(fcu.modifiers[0]) #fcu.modifiers.update() if cycle_mod: continue fcu.modifiers.new('CYCLES') fcu.update() if not len(mod_list): continue #restore old modifiers paste_modifiers(fcu, mod_list) fcu.modifiers.update() redraw_areas(['GRAPH_EDITOR', 'VIEW_3D']) return {'FINISHED'} class RemoveFcurves(bpy.types.Operator): """Remove Cyclic Fcurve modifiers from all the selected bones and objects""" bl_idname = "anim.layer_cyclic_remove" bl_label = "Cyclic_Remove" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): if not context.object: return False anim_data = anim_data_type(context.object) if not hasattr(anim_data, 'action'): return False return anim_data.action is not None def execute(self, context): transform_types = ['location', 'rotation_euler', 'rotation_quaternion', 'scale'] for obj in context.selected_objects: anim_data = anim_data_type(obj) for fcu in anim_data.action.fcurves: if obj.mode == 'POSE': #apply only to selected bones if obj.als.only_selected_bones: bones = [bone.path_from_id() for bone in context.selected_pose_bones] if fcu.data_path.split('].')[0]+']' not in bones: continue if fcu.data_path in transform_types: continue # pose mode always applies to bones and object mode to objects. elif obj.mode != 'POSE' and obj.als.data_type != 'KEY': if fcu.data_path not in transform_types: continue if not multikey.filter_properties(obj, fcu): continue if len(fcu.modifiers): for mod in fcu.modifiers: if mod.type == 'CYCLES': fcu.modifiers.remove(mod) fcu.update() for area in context.window_manager.windows[0].screen.areas: if area.type == 'GRAPH_EDITOR' or area.type == 'VIEW_3D': area.tag_redraw() break return {'FINISHED'} class ResetLayerKeyframes(bpy.types.Operator): """Add keyframes with 0 Value to the selected object/bones in the current layer, usefull for additive layers""" bl_idname = "anim.layer_reset_keyframes" bl_label = "Reset_Layer_Keyframes" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.object and len(context.object.Anim_Layers) def execute(self, context): obj = context.object anim_data = anim_data_type(obj) transform_types = ['location', 'rotation_euler', 'rotation_quaternion', 'scale'] fcurves = anim_data.action.fcurves frame_current = context.scene.frame_current strip = anim_data.nla_tracks[obj.als.layer_index].strips[0] frame_current = round(bake_ops.frame_evaluation(frame_current, strip), 3) for fcu in fcurves: if obj.type == 'ARMATURE': #apply only to selected bones if obj.mode == 'POSE' and fcu.data_path in transform_types: #skip continue elif obj.mode == 'POSE' and obj.als.only_selected_bones: bones = [bone.path_from_id() for bone in context.selected_pose_bones] if fcu.data_path.split('].')[0]+']' not in bones:# and fcu.data_path not in transform_types: continue elif obj.mode == 'OBJECT' and fcu.data_path not in transform_types: continue if not multikey.filter_properties(obj, fcu): continue key_exists = False blend_types = {'REPLACE', 'COMBINE'} value = bake_ops.attr_default(obj, (fcu.data_path, fcu.array_index))[fcu.array_index] if strip.blend_type in blend_types else 0 #check if a key already exists on in the current frame for key in fcu.keyframe_points: if round(key.co[0], 3) == frame_current: key.co[1] = value key_exists = True fcu.update() continue if key_exists: continue #if key doesnt exists then add keyframes in current frame fcu.keyframe_points.add(1) try: fcu.keyframe_points[-1].co = (frame_current, value) except TypeError: print('Type Error ', fcu.data_path, frame_current, value) fcu.update() return {'FINISHED'} class AddAction(bpy.types.Operator): """Add a new action""" bl_idname = "anim.add_action" bl_label = "Add New Action" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): obj = context.object anim_data = anim_data_type(obj) tweak_mode_upper_stack(context, obj, anim_data, enter = False) #If there is no action get the layer name if anim_data.nla_tracks[obj.als.layer_index].strips[0].action is None: action = bpy.data.actions.new(obj.Anim_Layers[obj.als.layer_index].name) #otherwise get the previous action name else: action = obj.Anim_Layers[obj.als.layer_index].action action = bpy.data.actions[action.name].copy() obj.Anim_Layers[obj.als.layer_index].action = action #go into tweak mode obj.als.layer_index = obj.als.layer_index return {'FINISHED'} class RemoveAction(bpy.types.Operator): """remove the action from the layer""" bl_idname = "anim.remove_action" bl_label = "Remove Action" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): obj = context.object anim_data = anim_data_type(obj) tweak_mode_upper_stack(context, obj, anim_data, enter = False) anim_data.nla_tracks[obj.als.layer_index].strips[0].action = None obj.Anim_Layers[obj.als.layer_index].action = 'None' # action_items(obj.Anim_Layers[obj.als.layer_index], context) # obj.Anim_Layers[obj.als.layer_index].action = action.name obj.als.layer_index = obj.als.layer_index return {'FINISHED'} def sync_frame_range(context): """Sync Frame Range to Action Length""" obj = context.object anim_data = anim_data_type(obj) use_frame_range = False strip = anim_data.nla_tracks[obj.als.layer_index].strips[0] layer = obj.Anim_Layers[obj.als.layer_index] action = strip.action action_range = action.frame_range[1] - action.frame_range[0] #strip_range = strip.frame_end - strip.frame_start offset = layer.offset if action.use_frame_range: use_frame_range = True action.use_frame_range = False layer.frame_start = action.frame_range[0] + offset layer.frame_end = action.frame_range[0] + offset + (action_range * strip.scale * strip.repeat) strip.action_frame_start = action.frame_range[0] strip.action_frame_end = action.frame_range[1] if use_frame_range: action.use_frame_range = True class SyncActionLength(bpy.types.Operator): """Sync Frame Range to Action Length""" bl_idname = "anim.sync_frame_range" bl_label = "Sync to Action" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): anim_data = anim_data_type(context.object) if not hasattr(anim_data,"nla_tracks"): return False if not len(anim_data.nla_tracks[context.object.als.layer_index].strips): return False return not anim_data.nla_tracks[context.object.als.layer_index].strips[0].use_sync_length def execute(self, context): sync_frame_range(context) return {'FINISHED'} class LAYERS_UL_list(bpy.types.UIList): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, reversed): obj = bpy.context.object anim_data = anim_data_type(obj) nla_tracks = anim_data.nla_tracks self.use_filter_sort_reverse = True if self.layout_type in {'DEFAULT', 'COMPACT'}: #row = layout.row() row = layout.row(align = True) icon = 'SOLO_ON' if nla_tracks[index].is_solo else 'SOLO_OFF' #row.prop(item,'solo', text = '', invert_checkbox=False, icon = icon, emboss=False) row.prop(nla_tracks[index], 'is_solo', text = '', invert_checkbox=False, icon = icon, emboss=False) split = row.split(factor=0.2, align = True) # split.prop(item, "action_list", icon_only = True, icon_value = 0, emboss = False)# #split.prop_enum(item, "action",value = item.action.name, text ='') #split.template_ID(item, "action", live_icon = False, new='', unlink='')#, emboss = False row.prop(item, "name", text="", emboss=False) split = row.split(factor=0, align = True) sub_row_right = row.row(align=True) sub_row_right.alignment = 'RIGHT' if len(nla_tracks[index].strips): blend_type = nla_tracks[index].strips[0].blend_type sub_row_right.label(text = blend_type[0] + ' ') # sub_row_right.prop_menu_enum(nla_tracks[index].strips[0], 'blend_type', text = blend_type[0]) icon = 'HIDE_ON' if nla_tracks[index].mute else 'HIDE_OFF' sub_row_right.prop(nla_tracks[index], 'mute', text = '', invert_checkbox=False, icon = icon, emboss=False) icon = 'LOCKED' if item.lock else 'UNLOCKED' sub_row_right.prop(item,'lock', text = '', invert_checkbox=False, icon = icon, emboss=False) # split = row.split(factor=0, align = True) elif self.layout_type in {'GRID'}: pass def invoke(self, context, event): pass class ANIMLAYERS_PT_Panel: bl_space_type = "VIEW_3D" bl_region_type = "UI" bl_category = "Animation" #bl_options = {"DEFAULT_CLOSED"} @classmethod def poll(cls, context): return context.object is not None class ANIMLAYERS_PT_List(ANIMLAYERS_PT_Panel, bpy.types.Panel): bl_label = "Animation Layers" bl_idname = "ANIMLAYERS_PT_List" def draw(self, context): obj = context.object anim_data = anim_data_type(obj) layout = self.layout addon_updater_ops.check_for_update_background() # could also use your own custom drawing # based on shared variables # call built-in function with draw code/checks addon_updater_ops.update_notice_box_ui(self, context) row = layout.row() row.prop(obj.als, 'turn_on') if not obj.als.turn_on: return #action type if hasattr(obj.data, 'shape_keys'): split = layout.split(factor=0.4, align = True) split.label(text = 'Data Type:') split.prop(obj.als, 'data_type', text ='') row = layout.row() row.template_list("LAYERS_UL_list", "", context.object, "Anim_Layers", context.object.als, "layer_index", rows=2) col = row.column(align=True) col.operator('anim.add_anim_layer', text="", icon = 'ADD') col.operator('anim.remove_anim_layer', text="", icon = 'REMOVE') col.separator() col.operator("anim.layer_move_up", text="", icon = 'TRIA_UP') col.operator("anim.layer_move_down", text="", icon = 'TRIA_DOWN') if not hasattr(anim_data, 'nla_tracks') or not len(obj.Anim_Layers):# or obj.Anim_Layers[obj.als.layer_index].lock: return track = anim_data.nla_tracks[obj.als.layer_index] col=layout.column(align = True) row = col.row() if not len(track.strips): return if not len(track.strips[0].fcurves): return if len(track.strips[0].fcurves[0].keyframe_points) and not obj.Anim_Layers[obj.als.layer_index].influence_mute: row.prop(track.strips[0], 'influence', slider = True, text = 'Influence') else: row.prop(obj.Anim_Layers[obj.als.layer_index], 'influence', slider = True, text = 'Influence') icon = 'KEY_DEHLT' if track.strips[0].fcurves[0].mute else 'KEY_HLT' row.prop(obj.Anim_Layers[obj.als.layer_index],'influence_mute', invert_checkbox = True, expand = True, icon_only=True, icon = icon, icon_value = 1) row = layout.row() row.prop(track.strips[0], 'blend_type', text = 'Blend') class ANIMLAYERS_PT_Ops(ANIMLAYERS_PT_Panel, bpy.types.Panel): bl_label = "Bake Operators" bl_idname = "ANIMLAYERS_PT_Ops" bl_parent_id = 'ANIMLAYERS_PT_List' bl_options = {"DEFAULT_CLOSED"} def draw(self, context): obj = context.object if obj is None: return if not obj.als.turn_on: return anim_data = anim_data_type(obj) if not hasattr(anim_data, 'nla_tracks') or not len(obj.Anim_Layers):# or obj.Anim_Layers[obj.als.layer_index].lock: return layout = self.layout merge_layers = layout.column() #merge_layers.operator("anim.layers_merge_down", text="New Baked Layer", icon = 'NLA') merge_layers.operator("anim.layers_merge_down", text="Merge / Bake", icon = 'NLA_PUSHDOWN') duplicateanimlayer = layout.row(align=True) duplicateanimlayer.operator('anim.duplicate_anim_layer', text="Duplicate Layer", icon = 'SEQ_STRIP_DUPLICATE') icon = 'LINKED' if obj.als.linked else 'UNLINKED' duplicateanimlayer.prop(obj.als, 'linked', icon_only=True, icon = icon) extract = layout.row(align=True) extract.operator('anim.extract_selected_bones', text="Extract Selected Bones", icon = 'SELECT_SUBTRACT') markers = layout.row(align=True) markers.operator('anim.extract_markers', text="Extract Marked Keyframes", icon = 'MARKER_HLT') class ANIMLAYERS_PT_Tools(ANIMLAYERS_PT_Panel, bpy.types.Panel): bl_label = "Layer Tools" bl_idname = "ANIMLAYERS_PT_Tools" bl_parent_id = 'ANIMLAYERS_PT_List' bl_options = {"DEFAULT_CLOSED"} def draw(self, context): obj = context.object if obj is None: return if not obj.als.turn_on: return if len(obj.Anim_Layers): if obj.Anim_Layers[obj.als.layer_index].lock: return layout = self.layout row = layout.row() row.operator("anim.bones_in_layer", text="Select Bones in Layer", icon = 'BONE_DATA') row = layout.row() row.separator() row = layout.row() split = row.split(factor=0.9, align = True) #if obj.mode == 'POSE': split.prop(obj.als, 'only_selected_bones', text = 'Affect Only Selected Bones')#, icon = 'GROUP_BONE' #else: # split.label(text = 'Filter') split.operator('fcurves.filter', icon ='FILTER', text = '') box = layout.box() row = box.row() row.operator("anim.layer_reset_keyframes", text="Reset Key Layer ", icon = 'KEYTYPE_MOVING_HOLD_VEC') row = box.row() row.prop(obj.als, 'inbetweener', text = 'Inbetweener', slider = True) layout.separator(factor = 0.2) row = layout.row() row.operator('anim.share_layer_keys', text = 'Share Layer Keys') row.prop(obj.als, 'share_layer_keys', text = '') layout.separator(factor = 0.2) box = layout.box() row = box.row() row.alignment = 'CENTER' row.label(text = 'Multikey - Edit Multiple Keyframes') row = box.row() row.prop(context.scene.multikey, 'scale', text = 'Scale', slider = True) row.prop(context.scene.multikey, 'randomness', text = 'Random', slider = True) box.operator("fcurves.multikey", icon = 'ACTION_TWEAK') layout.separator(factor = 0.2) row = layout.row() row.operator("anim.layer_cyclic_fcurves", text="Cyclic Fcurves", icon = 'FCURVE') row.operator("anim.layer_cyclic_remove", text="Remove Fcurves", icon = 'X') layout.separator(factor = 0.2) box = layout.box() row = box.row() #row.label(text= 'Keyframes From Multiple Layers:') row.prop(obj.als, 'view_all_keyframes', text = 'View Multiple Layer Keyframes') if obj.als.view_all_keyframes: row = box.row() split = row.split(factor=0.4, align = True) split.prop(obj.als, 'edit_all_keyframes') split.prop_menu_enum(obj.als, 'view_all_type') # class ANIMLAYERS_PT_Multikey(ANIMLAYERS_PT_Panel, bpy.types.Panel): # bl_label = "Multikey" # bl_idname = "ANIMLAYERS_PT_Multikey" # bl_parent_id = "ANIMLAYERS_PT_Tools" # bl_options = {"DEFAULT_CLOSED"} # def draw(self, context): # layout = self.layout # #layout.label(text="Multikey panel") # #layout.prop(context.scene.multikey, 'handletype') # #layout.separator() # #layout.label(text="Edit all selected keyframes") # #split = layout.split(factor=0.1, align = True) # #split.operator('fcurves.filter', icon ='FILTER', text = '') class ANIMLAYERS_PT_Settings(ANIMLAYERS_PT_Panel, bpy.types.Panel): bl_label = "Layer Settings" bl_idname = "ANIMLAYERS_PT_Settings" bl_parent_id = 'ANIMLAYERS_PT_List' bl_options = {"DEFAULT_CLOSED"} def draw(self, context): obj = context.object if obj is None: return if not obj.als.turn_on: return # if len(obj.Anim_Layers): # if obj.Anim_Layers[obj.als.layer_index].lock: # return anim_data = anim_data_type(obj) if not hasattr(anim_data, 'nla_tracks'): return nla_tracks = anim_data.nla_tracks if not len(nla_tracks): return track = nla_tracks[obj.als.layer_index] layer = obj.Anim_Layers[obj.als.layer_index] layout = self.layout box = layout.box() if anim_data is not None: row = box.row(align = True) row.alignment = 'CENTER' row.label(text = 'Active Action: ') row.template_ID(layer, "action", new="anim.add_action") #, new="action.new", unlink="action.unlink" row = box.row(align = True) split = row.split(factor=0.6, align = True) split.prop(obj.als, 'auto_rename', text = 'Sync Layer/Action Names') split.prop(obj.als, 'auto_blend') box = layout.box() ##Custom Frame Range row = box.row() row.prop(layer,'frame_range', icon = 'TIME') frame_range_settings = context.preferences.addons[__package__].preferences.frame_range_settings if layer.frame_range: # row.operator("anim.sync_frame_range", text="", icon = 'FILE_REFRESH') row = box.row() if frame_range_settings == 'ANIMLAYERS': row.prop(layer,'frame_start', text = 'Frame Start') row.prop(layer,'frame_end', text = 'Frame End') if len(track.strips): row.prop(track.strips[0],'extrapolation', text = '') row = box.row() row.prop(track.strips[0],'use_sync_length', text = 'Always Sync') row.operator("anim.sync_frame_range", text="Sync to Action", icon = 'FILE_REFRESH') row = box.row() row.prop(track.strips[0],'use_reverse') #row.prop(track.strips[0],'repeat') row.prop(layer,'repeat') elif len(track.strips): row.prop(track.strips[0],'frame_start', text = 'Frame Start') row.prop(track.strips[0],'frame_end', text = 'Frame End') row.prop(track.strips[0],'extrapolation', text = '') row = box.row() row.prop(track.strips[0],'action_frame_start', text = 'Action Start') row.prop(track.strips[0],'action_frame_end', text = 'Action End') row.prop(track.strips[0],'scale', text = 'Scale') row = box.row() row.prop(track.strips[0],'use_sync_length', text = 'Always Sync') row.operator("anim.sync_frame_range", text="Sync to Action", icon = 'FILE_REFRESH') row = box.row() row.prop(track.strips[0],'use_reverse') row.prop(layer,'repeat') row = box.row() # row.prop(track.strips[0],'scale', text = 'Scale') if frame_range_settings == 'ANIMLAYERS' or not layer.frame_range: row.prop(layer,'speed', text = 'Speed ') row.prop(layer,'offset', text = 'Offset') # split = layout.split(factor=0.6, align = True) # split.label(text="Default Blend Type ") # split.prop(context.scene.als,'blend_type', text = '') classes = (ResetLayerKeyframes, LAYERS_UL_list, AddAnimLayer, ExtractSelection, ExtractMarkers, DuplicateAnimLayer, RemoveAnimLayer, CyclicFcurves, RemoveFcurves, MoveAnimLayerUp, MoveAnimLayerDown, SelectBonesInLayer, ANIMLAYERS_PT_List, ANIMLAYERS_PT_Ops, ANIMLAYERS_PT_Tools, ANIMLAYERS_PT_Settings, ClearNLA, ClearActiveAction, OverrideError, AddAction, SyncActionLength, RemoveAction, ShareLayerKeys) # ANIMLAYERS_PT_Multikey def register(): from bpy.utils import register_class for cls in classes: register_class(cls) bpy.app.handlers.load_post.append(loadanimlayers) def unregister(): from bpy.utils import unregister_class for cls in classes: unregister_class(cls) if loadanimlayers in bpy.app.handlers.load_post: bpy.app.handlers.load_post.remove(loadanimlayers) if subscriptions.check_handler in bpy.app.handlers.depsgraph_update_pre: bpy.app.handlers.depsgraph_update_pre.remove(subscriptions.check_handler) if subscriptions.animlayers_frame in bpy.app.handlers.frame_change_post: bpy.app.handlers.frame_change_post.remove(subscriptions.animlayers_frame) bpy.msgbus.clear_by_owner(bpy.context.scene)