# ***** BEGIN GPL LICENSE BLOCK ***** # # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ***** END GPL LICENCE BLOCK ***** import bpy import gpu from gpu_extras.batch import batch_for_shader import bpy_extras.view3d_utils from . import Tools import math from mathutils import Vector from mathutils import Matrix from mathutils import Quaternion import numpy as np import copy import bisect import traceback import json import blf import inspect import time def coords_2d_update(self, context): '''update the 2d coordinates from the 3d coordinates, when the screen moves''' self.coords2d_ranges = [] self.coords2d_bones_frames = dict() self.coords2d_handles_r = dict() self.coords2d_handles_l = dict() #when pressing ctrl, use all points to hover, for adding new keyframe points if self.ctrl: self.coords2d_bones_frames = coords_2d_add(self, context, self.bones_allframes_coords) # bones_frames_coords = self.bones_allframes_coords else: #otherwise get only the keyframes self.coords2d_bones_frames = coords_2d_add(self, context, self.bones_keyframes_coords) #and coordinates of the handles if self.bones_handles_right: self.coords2d_handles_r = coords_2d_add(self, context, self.bones_handles_right) self.coords2d_handles_l = coords_2d_add(self, context, self.bones_handles_left) def coords_2d_add(self, context, bones_frames_coords): '''Get all the coordinates withing the range value from keys or handles''' range_value = self.range_value coords2d_bones_frames = dict() for obj_bonename, keyframes_coords in bones_frames_coords.items(): for frame, coord_3d in keyframes_coords.items(): coord_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, coord_3d) if not coord_2d: continue coord_2d = tuple(coord_2d) coord_x = int(coord_2d[0]) coord_y = int(coord_2d[1]) #continue if the coordinates are out of the screen if coord_x < 0 or coord_y < 0: continue # adding all the range values from each point based on the range values self.coords2d_ranges += [(x, y) for x in range(coord_x - range_value, coord_x + range_value + 1) for y in range(coord_y - range_value, coord_y + range_value + 1)] if coord_2d in coords2d_bones_frames: bone_frame = coords2d_bones_frames[coord_2d] bone_frame.update({obj_bonename : frame}) coords2d_bones_frames.update({coord_2d : bone_frame}) else: bone_frame = {obj_bonename: frame} coords2d_bones_frames.update({coord_2d : bone_frame}) return coords2d_bones_frames def get_keyframe_coord2d(self): '''get the coordinates frame and bone where the mouse is currently hovering''' if (int(self.mouse_x), int(self.mouse_y)) in self.coords2d_ranges: #get both coordinates for the keyframes and for the handles coords2d_bones_frames = self.coords2d_bones_frames.copy() if self.coords2d_handles_r or self.coords2d_handles_l: coords2d_bones_frames.update(self.coords2d_handles_r.copy()) coords2d_bones_frames.update(self.coords2d_handles_l.copy()) #creating a dict with the coords and difference from mouse, using the difference as the key to find the coord coords_2d_dict = dict() for coord_2d, bone_frame in coords2d_bones_frames.items(): x_diff = abs(int(self.mouse_x) - int(coord_2d[0])) y_diff = abs(int(self.mouse_y) - int(coord_2d[1])) #get all the coords that are inside the range if x_diff <= self.range_value and y_diff <= self.range_value: coords_2d_dict.update({(x_diff, y_diff) : coord_2d}) if coords_2d_dict: #compare the coords in the range value to see which one is the closest to the mouse coord_2d_key = min(coords_2d_dict.keys()) coord_2d = coords_2d_dict[coord_2d_key] return coord_2d, coords2d_bones_frames[coord_2d] return None, None def mouse_hover_keyframe(self, event): '''get the property that recognizes when the mouse is hovering on a frame''' if self.press or self.scale or self.rotate: return if (int(self.mouse_x), int(self.mouse_y)) not in self.coords2d_ranges: if any(self.hover_bone_frame.values()): #give None to all the hover values when the mouse is out of range self.hover_bone_frame = {key : None for key in self.hover_bone_frame.keys()} self.current_hover_frame = (None, None) self.update = True Tools.redraw_areas(['VIEW_3D']) return coords_2d, bone_frame = get_keyframe_coord2d(self) if coords_2d in self.coords2d_bones_frames: self.hover_bone_frame = {key : bone_frame if key == 'keyframe' else None for key in self.hover_bone_frame.keys()} key = 'keyframe' elif coords_2d in self.coords2d_handles_r: self.hover_bone_frame = {key : bone_frame if key == 'handle_r' else None for key in self.hover_bone_frame.keys()} key = 'handle_r' elif coords_2d in self.coords2d_handles_l: self.hover_bone_frame = {key : bone_frame if key == 'handle_l' else None for key in self.hover_bone_frame.keys()} key = 'handle_l' frame = next(iter(bone_frame.values())) # Make sure to update the motion path ONLY when leaving the current hovered frame # Or getting closer to another frame, or switching from keyframe to handle if self.current_hover_frame[0] != key or self.current_hover_frame[1] != frame: self.current_hover_frame = (key, frame) self.update = True Tools.redraw_areas(['VIEW_3D']) def filter_axis_distance(self, points, initial_coords): '''Filtering the X Y Z from the location while scaling or moving the keyframes''' if not self.scale and not self.press and not self.rotate: return points if self.filter_axis == '': ('cancel filtering') return points new_points = initial_coords.copy() axis_array = {'X' : 0, 'Y' : 1, 'Z' : 2} filter_axis = self.text.split(' along world ')[1] for axis in filter_axis: i = axis_array[axis] new_points[i] = points[i] return new_points def filter_axis_keyframes(self, event): if not self.press and not self.scale and not self.rotate: return False # if self.filter_axis is not None: # return False if event.type not in {'X', 'Y', 'Z'}: return False if event.value != 'PRESS': return False if not self.bones_selected_keyframes: return False if not any(self.hover_bone_frame.values()): return False axis = 'XYZ'.replace(event.type, '') if self.shift else event.type filter_axis = ' along world ' + axis #Remove filter axis when double click on the axis self.filter_axis = '' if self.filter_axis == filter_axis else filter_axis if ' along world ' in self.text: self.text = self.text.split(' along world ')[0] self.text = self.text + self.filter_axis def move_rotate_scale_keyframes(self, event): esc = True if not any([self.press, self.rotate, self.scale]) and hasattr(self, 'draw_handle') else False # print('escape ', esc) Tools.update_notification(self, bpy.context, event, esc, fade_out = False) if event.type not in {'G','R', 'S'}: return False # if self.scale or self.rotate: # return False if event.value != 'PRESS': return False if not self.bones_selected_keyframes: return False if not any(self.hover_bone_frame.values()): return False obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self) #if it's not in the selected keyframes then skip if obj_bonename in selection: if frame not in selection[obj_bonename]: return False else: return False #scaling and rotating need minimum of 2 selected keyframes if len(self.bones_selected_keyframes[obj_bonename]) < 2 and event.type in {'R', 'S'}: return False initialize_g_r_s(self, event.type) if key == 'keyframe': self.initial_keyframe_coords = copy.deepcopy(self.bones_keyframes_coords) else: self.initial_handles_right = copy.deepcopy(self.bones_handles_right) self.initial_handles_left = copy.deepcopy(self.bones_handles_left) self.initial_coord = Vector(coord_3d) self.prev_event_value = None def initialize_g_r_s(self, transform): '''set the attribute for the operator and reset the others''' op = {'G' : 'press', 'R' : 'rotate', 'S' : 'scale'} text_dict = {'G' : 'Move Keyframes ', 'R' : 'Rotate Keyframes ', 'S' : 'Scale Keyframes '} if getattr(self, op[transform]): return # Setting self.press, self.rotate or self.press to be True setattr(self, op[transform], True) #list of the values of the other operators to check if any of them is True other_ops = {k : getattr(self, v) for k, v in op.items() if k != transform} if any(other_ops.values()): self.text = text_dict[transform] #Previous operators get False for k, v in other_ops.items(): setattr(self, op[k], False) self.filter_axis = '' self.bones_keyframes_coords = self.initial_keyframe_coords else: Tools.notification_invoke(self, bpy.context, text = text_dict[transform], size = 15) def scale_points(points, pivot, scale_factor): # for i, point in enumerate(points): direction = points - pivot scaled_points = pivot + direction * scale_factor return scaled_points def project_onto_view_plane(vector, view_matrix): # Get the view direction from the view matrix view_direction = view_matrix.to_quaternion() @ Vector((0.0, 0.0, -1.0)) # Project the vector onto the plane perpendicular to the view direction projection = vector - vector.project(view_direction) return projection def get_rotation_2d(context, initial_coord, new_coord, pivot_2d): '''rotation the points in 2d''' new_coord_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, new_coord) initial_coord_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, initial_coord) vector1_2d = initial_coord_2d - pivot_2d vector2_2d = new_coord_2d - pivot_2d angle = vector1_2d.angle(vector2_2d) #check the direction it should rotate cross_product = vector1_2d.x * vector2_2d.y - vector1_2d.y * vector2_2d.x if cross_product > 0: angle *= -1 rotation_matrix = Matrix.Rotation(angle, 2, 'Z') return rotation_matrix, angle def get_location_path(posemode, obj, bonename): '''get the datapath of the location, check if it's a bone or object''' if posemode: posebone = obj.pose.bones[bonename] path = posebone.path_from_id() + '.location' else: path = 'location' return path def get_keyframe_locations(self, keyframe_coords): ''' get a list with the average of all the keyframe world locations''' locations = [] for obj_bonename, frames in self.bones_selected_keyframes.items(): for frame in frames: locations.append(keyframe_coords[obj_bonename][frame]) return locations def zoom_keyframes(self, context, event): if event.type != 'NUMPAD_PERIOD': return False if (int(self.mouse_x), int(self.mouse_y)) not in self.coords2d_ranges: return False locations = get_keyframe_locations(self, self.bones_keyframes_coords) avg_loc = sum(locations) / len(locations) context.region_data.view_location = avg_loc max_loc = max([Vector(loc) for loc in locations]) min_loc = min([Vector(loc) for loc in locations]) context.region_data.view_distance = (max_loc - min_loc).length if context.region_data.view_distance < 1: context.region_data.view_distance = 1 context.region_data.view_perspective = 'PERSP' return True def unpack_hover_frame(self): for key, value in self.hover_bone_frame.items(): if value is None: continue obj_bonename, frame = next(iter(value.items())) if key == 'keyframe': selection = self.bones_selected_keyframes coord_3d = self.bones_keyframes_coords[obj_bonename].get(frame) elif key == 'handle_r': selection = self.bones_selected_handles_r coord_3d = self.bones_handles_right[obj_bonename].get(frame) elif key == 'handle_l': selection = self.bones_selected_handles_l coord_3d = self.bones_handles_left[obj_bonename].get(frame) break return obj_bonename, key, frame, selection, coord_3d def left_mouse_press(self, context): '''behavior when pressing on the left mouse either to select or initialize moving the point''' if self.ctrl or self.scale or self.rotate: return False if self.press: return True if not any(self.hover_bone_frame.values()): return False #apply the selection either to the keyframes or handles obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self) self.initial_coord = Vector(coord_3d) #Checking if shift was pressed for multiple selections if self.shift: #ADD or remove selection using shift if obj_bonename in selection: #if holding SHIFT then DESELECT the frame, remove selection and handles if frame in selection[obj_bonename]: selection[obj_bonename].remove(frame) if key == 'keyframe': handles_frame_remove(self, obj_bonename, frame) if not len(selection[obj_bonename]): del selection[obj_bonename] # When removing selection returning instead of initialzing the movement self.update = True Tools.redraw_areas(['VIEW_3D']) # Update selection into the undo system context.scene.emp.selected_keyframes = serialize_dict(self.bones_selected_keyframes) bpy.ops.ed.undo_push(message = 'Select Motion Path Keyframe') return True else: # Add the new keyframe to selection selection[obj_bonename].append(frame) else: # Add the keyframe and object to the selection selection.update({obj_bonename : [frame]}) else: if obj_bonename in selection and frame in selection[obj_bonename]: # In case the frame is already selected, then it doesn't need to reset the selection # This is especially useful when selecting multiple keyframes and pressing to move them pass else: # If shift was not pressed then clear selection and create a new one clear_reselection(self, obj_bonename, key, frame, selection) # Initialize in order to start moving the keyframes when dragging the mouse get_handles(self) coords_2d_update(self, context) initialize_g_r_s(self, 'G') # Reset keyframe coords or handle coords if key == 'keyframe': self.initial_keyframe_coords = copy.deepcopy(self.bones_keyframes_coords) else: #create a copy of the handles in case operation is cancelled self.initial_handles_right = copy.deepcopy(self.bones_handles_right) self.initial_handles_left = copy.deepcopy(self.bones_handles_left) self.update = True Tools.redraw_areas(['VIEW_3D']) # Update selection into the undo system context.scene.emp.selected_keyframes = serialize_dict(self.bones_selected_keyframes) bpy.ops.ed.undo_push(message = 'Select Motion Path Keyframe') return True def clear_reselection(self, obj_bonename, key, frame, selection): if key == 'keyframe': selection.clear() handles_selection_clear(self) selection[obj_bonename] = [frame] def edit_motion_path_update(self, context, event): '''update mouse interaction with editable motion paths''' scene = context.scene if not self.coords2d_ranges: coords_2d_update(self, context) #if view angle is changed then update the 2d coords of the keyframess if self.prev_view_matrix != context.region_data.view_matrix or self.prev_window_matrix != context.region_data.window_matrix: self.prev_view_matrix = context.region_data.view_matrix.copy() self.prev_window_matrix = context.region_data.window_matrix.copy() coords_2d_update(self, context) mouse_hover_keyframe(self, event) # g_move_keyframes(self, event) move_rotate_scale_keyframes(self, event) filter_axis_keyframes(self, event) if event.type == self.select_mouse: #'LEFTMOUSE' frame_current = scene.frame_current_final #add or remove keyframes using Ctrl hotkey if event.value == 'RELEASE' and self.ctrl: if not self.hover_bone_frame['keyframe']: return True obj_bonename, frame = next(iter(self.hover_bone_frame['keyframe'].items())) key_frames = self.mp_bones_keys[obj_bonename] obj = context.view_layer.objects[obj_bonename[0]] # posebone = obj.pose.bones[obj_bonename[1]] path = get_location_path(self.posemode, obj, obj_bonename[1]) fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action) #if there is a keyframe then remove it if frame in key_frames: #removing keyframes for i in range(3): fcu = fcurves.find(data_path = path, index = i) if fcu is None: continue for keyframe in fcu.keyframe_points: if round(add_frame_offset(obj, keyframe.co[0])) == frame: fcu.keyframe_points.remove(keyframe) #deselect if selected if obj_bonename in self.bones_selected_keyframes: if frame in self.bones_selected_keyframes[obj_bonename]: self.bones_selected_keyframes[obj_bonename].remove(frame) handles_frame_remove(self, obj_bonename, frame) self.mp_bones_keys[obj_bonename].remove(frame) else: #if there is no keyframe then add a new one if frame != frame_current: scene.frame_set(int(frame), subframe = frame%1) #Offset is already applied to the frame obj.keyframe_insert(data_path = path, frame = frame) self.mp_bones_keys[obj_bonename].append(frame) self.mp_bones_keys[obj_bonename].sort() #reset the motion path update_mp_points(self, context, [frame], [obj_bonename]) update_keyframe_coords(self) calculate_velocities(self, context) # get_mp_points(self, context) coords_2d_update(self, context) self.current_bones_keys = get_current_bones_keys(self) scene.frame_set(int(frame_current), subframe = frame_current % 1) #update selected keyframes property serialization for undo purpose context.scene.emp.selected_keyframes = serialize_dict(self.bones_selected_keyframes) bpy.ops.ed.undo_push(message = 'Add / Remove Keyframe') self.update = True return True if event.value == 'PRESS': if self.prev_event_value != 'NOTHING': self.prev_event_value = event.value if left_mouse_press(self, context): return True if event.value == 'RELEASE' and (self.press or self.scale or self.rotate): if self.prev_event_value == 'PRESS' and not self.shift: # When releasing directly after press it will cancel and reselect obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self) clear_reselection(self, obj_bonename, key, frame, selection) get_handles(self) coords_2d_update(self, context) cancel_update(self) self.prev_event_value = event.value return True ###Apply the keyframe movement### #get the new coordinates and distance from the selected frame #Using obj_bonename for hovered object to get the distance, and the selected_obj_bonename # for the rest of the selected bones obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self) for selected_obj_bonename, frames in self.bones_selected_keyframes.items(): objname, bonename = selected_obj_bonename obj = context.view_layer.objects[objname] #if we are using an object then treat the object as a bone when getting the distance posebone = obj.pose.bones[bonename] if self.posemode else obj #get the distance from the initial coords if self.press: #get only a single distance when only moving/grabing the keyframes frame_distance = get_distance(key, self, context, posebone, obj_bonename, frame, self.matrix_diff[selected_obj_bonename], coord_3d) elif self.scale or self.rotate: # get the distance on each frame separatly frame_distance = dict() for frame in frames: distance = get_distance(key, self, context, posebone, obj_bonename, frame, self.matrix_diff[selected_obj_bonename], coord_3d) frame_distance.update({frame : distance}) #apply the coordinates and distance to all the selected keyframes and handles mp_update_keyframes(self, context, selected_obj_bonename, frame_distance, key) # if scene.frame_current_final != frame_current: context.scene.frame_set(int(frame_current), subframe = frame_current % 1) update_frames = [frame for frames in self.bones_selected_keyframes.values() for frame in frames] update_mp_points(self, context, update_frames, self.bones_selected_keyframes.keys(), key, coord_3d) update_keyframe_coords(self) calculate_velocities(self, context) self.coords2d_bones_frames = None coords_2d_update(self, context) # self.hover_bone_frame = {'keyframe': None, 'handle_r' : None, 'handle_l' : None} # self.moving_frame = dict() self.initial_coord = None self.initial_keyframe_coords = None self.prev_event_value = event.value self.press = False self.scale = False self.rotate = False self.filter_axis = '' self.current_bones_keys = get_current_bones_keys(self) self.update = True context.scene.emp.selected_keyframes = serialize_dict(self.bones_selected_keyframes) bpy.ops.ed.undo_push(message = 'Move Motion Path Keyframe') return True elif event.type == 'MOUSEMOVE' and (self.press or self.scale or self.rotate): #updating only the moving keyframe obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self) #get the new coordinates and distance from the selected moving keyframe loc_coord = Vector(bpy_extras.view3d_utils.region_2d_to_location_3d(context.region, context.region_data, (self.mouse_x, self.mouse_y), coord_3d)) distance = loc_coord - Vector(coord_3d) # context.scene.cursor.location = loc_coord if not self.press: locations = get_keyframe_locations(self, self.initial_keyframe_coords) pivot = Vector(sum(locations) / len(locations)) if self.press: #adding text with all the detail axis_array = {0 : 'X', 1 : 'Y', 2 : 'Z'} if self.filter_axis == '': move_distance = loc_coord - self.initial_coord else: move_distance = (value if axis_array[i] in self.filter_axis else 0.0 for i, value in enumerate (loc_coord - self.initial_coord)) distance_text = ' '.join(axis_array[i] + ': ' + str(round(value, 2)) for i, value in enumerate(move_distance)) self.text = 'Move Keyframes ' + distance_text + self.filter_axis elif self.rotate: pivot_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, pivot) rotation_matrix, angle = get_rotation_2d(context, self.initial_coord, loc_coord, pivot_2d) self.text = 'Rotate Keyframes ' + str(round(math.degrees(angle), 2)) + self.filter_axis elif self.scale: try: scale_factor = (loc_coord - pivot).length / (self.initial_coord - pivot).length self.text = 'Scale Keyframes ' + str(round(scale_factor, 2)) + self.filter_axis except ZeroDivisionError: return True # Check the direction using the dot product dot_product = (loc_coord - pivot).dot(self.initial_coord - pivot) dot = -1 if dot_product < 0 else 1 #add the distance to all the selected keyframes or the selected handles for obj_bonename in selection.keys(): if key == 'keyframe': keyframes_coords = self.bones_keyframes_coords[obj_bonename] #get all the points from the curve of this bone points = list(self.bones_points[obj_bonename]) frames_distance = dict() for frame, coord_3d in keyframes_coords.items(): if frame not in selection[obj_bonename]: continue initial_coords = Vector(self.initial_keyframe_coords[obj_bonename][frame]) #get the index of the point based on the frame number i = (round(frame - self.avz_frame_start[obj_bonename])) *3 coords = Vector(points[i : i+3]) if Vector(coord_3d) != Vector(coords): continue if self.press: new_coord = initial_coords + (Vector(loc_coord) - self.initial_coord) elif self.scale: #scale only the initial coords and add it as a distance new_coord = scale_points(initial_coords, pivot, scale_factor * dot) elif self.rotate: initial_coord_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, initial_coords) rotated_vector = (initial_coord_2d - pivot_2d) @ rotation_matrix + pivot_2d new_coord = Vector(bpy_extras.view3d_utils.region_2d_to_location_3d(context.region, context.region_data, rotated_vector, coord_3d)) new_coord = filter_axis_distance(self, new_coord, initial_coords) #get the distance of the handles from the keyframe last position distance = Vector(new_coord - Vector(self.bones_points[obj_bonename][i:i+3])) frames_distance.update({frame : distance}) #apply the keyframe scaled position self.bones_points[obj_bonename][i:i+3] = new_coord if obj_bonename in self.bones_handles_right: #get the distance from the keyframes self.bones_handles_right[obj_bonename][frame] += distance self.bones_handles_left[obj_bonename][frame] += distance add_distance_to_points(self, obj_bonename, frames_distance) else: handle_coords_r = self.bones_handles_right[obj_bonename] handle_coords_l = self.bones_handles_left[obj_bonename] #Move the right handle for frame, coord_3d in handle_coords_r.items(): if obj_bonename not in self.bones_selected_handles_r: continue if frame not in self.bones_selected_handles_r[obj_bonename]: continue #in automatic handles move only one of them if get_side_auto(self, obj_bonename, frame, key) == 'handle_l': continue keyframe_coord = self.bones_keyframes_coords[obj_bonename][frame] try: dist_len = abs((handle_coords_r[frame] + distance - Vector(keyframe_coord)).length / (handle_coords_r[frame] - Vector(keyframe_coord)).length) except ZeroDivisionError: dist_len = None handle_coords_r[frame] += distance if self.bones_handles_types[obj_bonename][frame] == 'AUTO': handle_coords_l[frame] = align_handles(handle_coords_l[frame], coord_3d, Vector(keyframe_coord), dist_len) # handle_coords_l[frame] -= distance #For Auto Clamped move the oppposite direction #Move the Left handle for frame, coord_3d in handle_coords_l.items(): if obj_bonename not in self.bones_selected_handles_l: continue if frame not in self.bones_selected_handles_l[obj_bonename]: continue #in automatic handles move only one of them if get_side_auto(self, obj_bonename, frame, key) == 'handle_r': continue keyframe_coord = self.bones_keyframes_coords[obj_bonename][frame] #get the distance length before adding the distance to the handles try: dist_len = abs((handle_coords_l[frame] + distance - Vector(keyframe_coord)).length / (handle_coords_l[frame] - Vector(keyframe_coord)).length) except ZeroDivisionError: dist_len = None handle_coords_l[frame] += distance if self.bones_handles_types[obj_bonename][frame] == 'AUTO': handle_coords_r[frame] = align_handles(handle_coords_r[frame], coord_3d, Vector(keyframe_coord), dist_len) # handle_coords_r[frame] -= distance #For Auto Clamped move the oppposite direction self.prev_event_value = event.value self.update = True Tools.redraw_areas(['VIEW_3D']) return True #Cancel the operation, usually with RIGHTMOUSE go back to initial coordinates elif event.type in {self.cancel_mouse, 'ESC'} and (self.press or self.scale or self.rotate): cancel_update(self) return True elif (event.type == 'LEFT_CTRL' or event.type == 'RIGHT_CTRL'): if event.value == 'RELEASE' and self.ctrl: #Go out of Ctrl mode self.bones_allframes_coords = dict() self.ctrl = False coords_2d, bone_frame = get_keyframe_coord2d(self) if coords_2d in self.coords2d_bones_frames: #remove extra keyframes that were added during hovering with ctrl self.hover_bone_frame['keyframe'] = bone_frame bone, frame = next(iter(bone_frame.items())) #if the keyframe was added only during hovering with ctrl but not actually added, then remove it if frame in self.bones_keyframes_coords[bone] and frame not in self.mp_bones_keys[bone]: self.bones_keyframes_coords[bone].pop(frame) self.coords2d_bones_frames[coords_2d].pop(bone) coords_2d_update(self, context) self.update = True Tools.redraw_areas(['VIEW_3D']) return True elif event.value == 'PRESS' and not self.ctrl: #Setting up adding keyframe mode self.ctrl = True self.bones_allframes_coords = dict() #Create self.bones_allframes_coords that is similiar to self.bones_keyframes_coords for obj_bonename, points in self.bones_points.items(): for i in range(0, len(points), 3): frame = ((i+3) / 3) + self.avz_frame_start[obj_bonename] - 1 if obj_bonename in self.bones_allframes_coords: # if frame in self.bones_allframes_coords[obj_bonename]: self.bones_allframes_coords[obj_bonename].update({frame : points[i : i+3]}) else: self.bones_allframes_coords.update({obj_bonename : {frame : points[i : i+3]}}) coords_2d_update(self, context) coords_2d, bone_frame = get_keyframe_coord2d(self) if coords_2d in self.coords2d_bones_frames: self.hover_bone_frame['keyframe'] = bone_frame self.update = True Tools.redraw_areas(['VIEW_3D']) return True return False def cancel_update(self): '''Cancelling the operation, moving the points to their initial position''' obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self) if key == 'keyframe': for obj_bonename in selection.keys(): #get the coordinates from bones_keyframes_coords keyframes_coords = self.bones_keyframes_coords[obj_bonename] frames_distance = dict() for frame, coord_3d in keyframes_coords.items(): if frame not in self.bones_selected_keyframes[obj_bonename]: continue # print(f'distance {distance} coord_3d {Vector(coord_3d)} initial_keyframe_coords {Vector(self.initial_keyframe_coords[obj_bonename][frame])}') distance = Vector(coord_3d) - Vector(self.initial_keyframe_coords[obj_bonename][frame]) #get all the points from the curve of this bone #get the index of the point based on the frame number i = (round(frame - self.avz_frame_start[obj_bonename])) *3 coords = Vector(self.bones_points[obj_bonename][i : i+3]) if Vector(coord_3d) == coords: self.bones_points[obj_bonename][i:i+3] = coords - distance frames_distance.update({frame : distance * -1}) if self.bones_handles_right: self.bones_handles_right[obj_bonename][frame] = self.bones_handles_right[obj_bonename][frame] - distance self.bones_handles_left[obj_bonename][frame] = self.bones_handles_left[obj_bonename][frame] - distance add_distance_to_points(self, obj_bonename, frames_distance) else: self.bones_handles_right = self.initial_handles_right self.bones_handles_left = self.initial_handles_left del self.initial_handles_right, self.initial_handles_left self.press = False self.scale = False self.rotate = False self.filter_axis = '' # self.moving_frame = dict() self.initial_coord = None self.initial_keyframe_coords = None # self.hover_bone_frame = {'keyframe': None, 'handle_r' : None, 'handle_l' : None} self.update = True Tools.redraw_areas(['VIEW_3D']) def add_frame_offset(obj, frame, add = False): '''Adding strip offset''' anim_data = obj.animation_data if not anim_data.use_nla or not anim_data.use_tweak_mode: return frame if not len(anim_data.nla_tracks): return frame offset = frame - anim_data.nla_tweak_strip_time_to_scene(frame) frame = frame + offset if add else frame - offset return frame def add_distance_to_points(self, obj_bonename, frames_distance): '''Adding the distance to the inbetween frames ''' for frame, distance in frames_distance.items(): frame_index = self.mp_bones_keys[obj_bonename].index(frame) current_i = (round(frame - self.avz_frame_start[obj_bonename])) *3 if frame_index: prev_key = round(self.mp_bones_keys[obj_bonename][frame_index -1], 2) if prev_key < 0: continue prev_dist = frames_distance[prev_key] if prev_key in frames_distance else Vector((0,0,0)) prev_frames = int(frame - prev_key) prev_i = (round(prev_key - self.avz_frame_start[obj_bonename])) *3 #Linear distribution of the points until the previous keyframe t = np.linspace(prev_dist, distance, prev_frames, endpoint=False) added_array = t.flatten()[3:] #apply the keyframe position if len(self.bones_points[obj_bonename][prev_i + 3:current_i]) != len(added_array): continue self.bones_points[obj_bonename][prev_i + 3:current_i] += added_array #Interpolatin for the next keyframe if (frame_index + 1) >= len(self.mp_bones_keys[obj_bonename]): continue next_key = round(self.mp_bones_keys[obj_bonename][frame_index + 1], 2) #if the next keyframe is also being moved then skip it if next_key in frames_distance: continue next_frames = int(next_key - frame) next_i = (round(next_key - self.avz_frame_start[obj_bonename])) *3 #Linear distribution of the points until the next keyframe t = np.linspace(distance, Vector((0,0,0)), next_frames, endpoint=False) #Flat the array to be 1 dimension added_array = t.flatten()[3:] if len(self.bones_points[obj_bonename][current_i + 3 : next_i]) != len(added_array): continue self.bones_points[obj_bonename][current_i + 3 : next_i] += added_array def draw_frames_callback_px(self, context): font_id = 0 # XXX, need to find out how best to get this. alpha = 0.75 for keyframes_coords in self.bones_keyframes_coords.values(): for frame, coord_3d in keyframes_coords.items(): coords2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, coord_3d) blf.position(font_id, coords2d[0],coords2d[1] + 10 ,0) blf.size(font_id, 15) blf.color(0, 1, 1, 0, alpha) blf.draw(font_id, str(int(round(frame))) ) context.area.tag_redraw() # Function to draw the motion path def draw_motionpath_callback_px(self, context): '''draw the motion path''' # start_time = time.perf_counter() emp = context.scene.emp if emp.vis_type =='BEFORE_AFTER' or emp.frame_range == 'AROUND': # Checking for frame change, updating here because modal operator is not updated during frame change if self.frame_current != context.scene.frame_current_final: self.frame_current = context.scene.frame_current_final self.update = True if self.update or emp.update: #Reset the lines and points # for k, v in self.batch_handles_points.items(): self.batch_handles_points = [] self.batch_handles_lines = [] handles_vertices = [] col_handles_points = [] handles_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR") pref = context.preferences.addons[__package__].preferences def set_alternate_vis_type(col_lines, col_points, frame): '''Setting the Alternating colors for the motion path''' if not frame % 2: col_lines.append(emp.color_after) col_points.append(emp.color_after) else: col_lines.append(emp.color_before) col_points.append(emp.color_before) def set_before_after_vis_type(col_lines, col_points, frame): '''Setting the colors for the motion path based on before and after current frame''' if frame > self.frame_current: col_lines.append(emp.color_after) col_points.append(emp.color_after) elif frame < self.frame_current: col_lines.append(emp.color_before) col_points.append(emp.color_before) else: col_lines.append((emp.color_after + emp.color_before) * 0.5) col_points.append((1.0, 1.0, 1.0)) def set_velocity_vis_type(col_lines, col_points, frame): '''Applying the velocity colors''' col_lines.append(self.bones_interpolated_colors[obj_bonename][frame]) col_points.append(self.bones_interpolated_colors[obj_bonename][frame]) def update_lines_points(): '''Updating the vertex and color information of motion path during iteration''' if not self.update and not emp.update: return #setup the hover attribute with the frame value. Either hover_keyframe, hover_handle_r or hover_handle_l hover_keyframe = None hover_handle_r = None hover_handle_l = None for key, value in self.hover_bone_frame.items(): if value is None: continue if obj_bonename in value: if key == 'keyframe': hover_keyframe = value[obj_bonename] elif key == 'handle_r': hover_handle_r = value[obj_bonename] elif key == 'handle_l': hover_handle_l = value[obj_bonename] #iterate every 3 points, since every point has a 3 coordinate system for i in range(0, len(points), 3): #every 3 points are a vector of a frame frame = ((i+3) / 3) + self.avz_frame_start[obj_bonename] - 1 #skip frames that are previous to the visual frames if frame < self.frame_start[obj_bonename]: continue if emp.frame_range == 'AROUND': before = self.frame_current - emp.before after = self.frame_current + emp.after-1 if frame < before or frame > after: continue coords = tuple(points[i : i+3]) vertices.append(coords) #append keyframes for a separate shader if frame in keyframes or frame == hover_keyframe: key_vertices.append(coords) #paint the current frame white if frame == self.frame_current: keyframe_color = (1.0, 1.0, 1.0, 1.0) else: keyframe_color = pref.mp_keyframe_color #(1.0, 1.0, 0.0, 1.0) if hover_keyframe == frame: if frame in keyframes and self.ctrl: #color the keyframe blue for removal keyframe_color = pref.mp_remove_color # (0.0, 0.5, 1.0, 1.0) else: #color the frame that is going to be added or selected as orange using hover color keyframe_color = pref.mp_hover_color # (1.0, 0.4, 0.0, 1.0) col_keys.append(keyframe_color) # Add the keyframe selection points if obj_bonename in self.bones_selected_keyframes: if frame in self.bones_selected_keyframes[obj_bonename]: # Color of the points of the selected keyframes col_sel_keys.append(pref.mp_key_selection_color) # col_sel_keys.append(emp.key_selection_color) sel_vertices.append(coords) # Add the handles selection points if emp.handles and self.bones_handles_right: #Add the handles to the selected keyframes if frame not in self.bones_handles_right[obj_bonename]: continue handles_vertices.append(self.bones_handles_right[obj_bonename][frame]) if hover_handle_r == frame: # Orange-red color when hovering over the handle points col_handles_points.append(pref.mp_hover_color) else: # Orange light color when not hovering col_handles_points.append(pref.mp_handle_color) #add the control points to the right handles if obj_bonename in self.bones_selected_handles_r: if frame in self.bones_selected_handles_r[obj_bonename]: col_sel_keys.append(pref.mp_handle_selection_color) sel_vertices.append(self.bones_handles_right[obj_bonename][frame]) handles_vertices.append(self.bones_handles_left[obj_bonename][frame]) if hover_handle_l == frame: # Orange-red color when hovering over the handle points col_handles_points.append(pref.mp_hover_color) # (1.0, 0.4, 0.2, 1.0) else: # Orange light color when not hovering col_handles_points.append(pref.mp_handle_color) # (1.0, 0.8, 0.2, 1.0) #add the control cage to the left handles if obj_bonename in self.bones_selected_handles_l: if frame in self.bones_selected_handles_l[obj_bonename]: # Adding color to selected handles col_sel_keys.append(pref.mp_handle_selection_color) sel_vertices.append(self.bones_handles_left[obj_bonename][frame]) # selected_coords, indices = draw_sphere(self.bones_handles_left[obj_bonename][frame], size) # batch_selected_keyframes.append(batch_for_shader(selected_shader, 'LINES', {"pos": selected_coords}, indices = indices)) #draw the line of the handles handles_line = [self.bones_handles_right[obj_bonename][frame]] + [coords] + [self.bones_handles_left[obj_bonename][frame]] self.batch_handles_lines.append(batch_for_shader(handles_shader, 'LINE_STRIP', {"pos": handles_line})) #add the colors for the motion path lines and points if emp.vis_type == 'ALTERNATE': set_alternate_vis_type(col_lines, col_points, frame) elif emp.vis_type == 'BEFORE_AFTER': set_before_after_vis_type(col_lines, col_points, frame) elif emp.vis_type == 'VELOCITY': set_velocity_vis_type(col_lines, col_points, frame) # print('update_lines_points ',obj_bonename, len(vertices)) def update_shaders(): if not self.update and not emp.update: return if emp.lines: self.batch_line[obj_bonename] = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": col_lines}) if emp.points: self.batch_points[obj_bonename] = batch_for_shader(shader_points, 'POINTS', {"pos": vertices, "color": col_points}) self.batch_keyframes[obj_bonename] = batch_for_shader(shader_points, 'POINTS', {"pos": key_vertices, "color": col_keys}) self.batch_selected[obj_bonename] = batch_for_shader(shader_points, 'POINTS', {"pos": sel_vertices, "color": col_sel_keys}) if handles_vertices: self.batch_handles_points = batch_for_shader(shader_points, 'POINTS', {"pos": handles_vertices, "color": col_handles_points}) for obj_bonename, points in self.bones_points.items(): #get the set of the keyframes frame numbers keyframes = self.mp_bones_keys[obj_bonename] vertices = [] key_vertices = [] sel_vertices = [] # keyframes_coords = dict() col_lines = [] col_points = [] col_keys = [] col_sel_keys = [] # self.batch_handles_points[obj_bonename] = [] # self.batch_handles_lines[obj_bonename] = [] # print(obj_bonename, ' resetting points') update_lines_points() # Make the motion path occluding object or not using the infront option if emp.infront: gpu.state.depth_test_set('NONE') else: gpu.state.depth_test_set('LESS_EQUAL') #Create all the shaders if emp.vis_type == 'BEFORE_AFTER': shader = gpu.shader.from_builtin("POLYLINE_SMOOTH_COLOR") else: shader = gpu.shader.from_builtin("POLYLINE_FLAT_COLOR") #From 4.5 shaders are changed, probably because of Vulkan shader_type = "FLAT_COLOR" if bpy.app.version < (4, 5, 0) else "POINT_FLAT_COLOR" shader_points = gpu.shader.from_builtin(shader_type) update_shaders() #draw the path and lines shader.bind() shader_points.bind() # gpu.state.line_width_set(10) gpu.state.point_size_set(emp.frame_size) region = context.region viewport_size = (region.width, region.height) # Set width of the lines shader.uniform_float("viewportSize", viewport_size) shader.uniform_float("lineWidth", emp.thickness) if emp.lines: self.batch_line[obj_bonename].draw(shader) if emp.points: self.batch_points[obj_bonename].draw(shader_points) gpu.state.point_size_set(emp.keyframe_size*2) self.batch_selected[obj_bonename].draw(shader_points) gpu.state.point_size_set(emp.keyframe_size) self.batch_keyframes[obj_bonename].draw(shader_points) #draw selected keyframes box controller and handles handles_shader.bind() handles_shader.uniform_float("viewportSize", viewport_size) handles_shader.uniform_float("color", (1, 1, 1, 1)) handles_shader.uniform_float("lineWidth", 1.0) # if obj_bonename in self.bones_selected_keyframes: # for batch_selection in batch_selected_keyframes: # batch_selection.draw(selected_shader) if self.batch_handles_lines: for batch_handle in self.batch_handles_lines: batch_handle.draw(handles_shader) # draw the points of the handles if self.batch_handles_points: self.batch_handles_points.draw(shader_points) self.update = emp.update = False # end_time = time.perf_counter() # draw_time = (end_time - start_time) * 1000 # Convert to milliseconds # print(f"Draw function took: {draw_time:.2f}ms") def calculate_velocities(self, context): emp = context.scene.emp if not emp.vis_type == 'VELOCITY': return # print('calculate_velocities ') self.bones_interpolated_colors = dict() # print('calculating velocities, comes from', inspect.stack()[1].function) for obj_bonename, points in self.bones_points.items(): # Reshaping all the points into array of vectors vectors = points.reshape(-1, 3) # Calculating all the velocities between all the vectors velocity_vectors = vectors[1:] - vectors[:-1] # print('velocities ', velocity_vectors) # Calculate speeds (magnitudes) for all vectors at once velocities = np.linalg.norm(velocity_vectors, axis = 1) if not np.any(velocities): continue normal_color_values = map_to_colors_percentile(velocities, context) # normal_color_values *= emp.velocity_factor # Convert the color values into the color vectors based on the selected colors interpolated_colors = interpolate_colors_vectorized(normal_color_values, emp.color_before, emp.color_after) # Duplicating the first color to match the number of points interpolated_colors = np.insert(interpolated_colors, 0, interpolated_colors[0], axis = 0) # Creating the frame numbers frames = np.arange(len(interpolated_colors)) + self.avz_frame_start[obj_bonename] # Mapping the frames to the colors using a dictionary self.bones_interpolated_colors.update({obj_bonename : dict(zip(frames, interpolated_colors))}) def map_to_colors_percentile(velocities, context): """Use percentiles instead of absolute min/max to clamp/limit values and also normalizing the values""" if len(velocities) == 0: return [] lower_percentile = context.scene.emp.clamp_min upper_percentile = context.scene.emp.clamp_max # Remove velocity vecotors that are 0, usually caused with empty frames in scene frame range filtered_velocities = velocities[velocities != 0] min_speed = np.percentile(filtered_velocities, lower_percentile) max_speed = np.percentile(filtered_velocities, upper_percentile) #Clamp velocity to not get hard spikes that will tone down the rest of the velocities clamped = np.clip(velocities, min_speed, max_speed) if max_speed != min_speed: color_values = (clamped - min_speed) / (max_speed - min_speed) else: color_values = np.full_like(velocities, 0.5) velocity_factor = context.scene.emp.velocity_factor if velocity_factor != 1.0: # Factor > 1.0: increases contrast (sharper transitions) # Factor < 1.0: decreases contrast (smoother transitions) color_values = np.power(color_values, velocity_factor) return color_values def interpolate_colors_vectorized(color_values, color_before, color_after): """Vectorized color interpolation""" color_before = np.array(color_before) color_after = np.array(color_after) # Expand color_values to match RGB dimensions t = color_values[:, np.newaxis] # Shape: (N, 1) # Linear interpolation for all colors at once interpolated = (1 - t) * color_after + t * color_before return interpolated def draw_cube(coords, size): # size = (sum(context.view_layer.objects[self.obj_name].dimensions) / 3) * 0.1 selected_coords = ( (coords[0]-size, coords[1]-size, coords[2]-size), (coords[0]+size, coords[1]-size, coords[2]-size), (coords[0]-size, coords[1]+size, coords[2]-size), (coords[0]+size, coords[1]+size, coords[2]-size), (coords[0]-size, coords[1]-size, coords[2]+size), (coords[0]+size, coords[1]-size, coords[2]+size), (coords[0]-size, coords[1]+size, coords[2]+size), (coords[0]+size, coords[1]+size, coords[2]+size)) indices = ( (0, 1), (0, 2), (1, 3), (2, 3), (4, 5), (4, 6), (5, 7), (6, 7), (0, 4), (1, 5), (2, 6), (3, 7)) return selected_coords, indices def draw_sphere(coords, size): vertices = ((0.0, 0.40137654542922974, 0.0), (-0.10388384014368057, 0.3876999318599701, 0.0), (-0.20068827271461487, 0.34760206937789917, 0.0), (-0.2838158905506134, 0.2838158905506134, 0.0), (-0.34760230779647827, 0.20068813860416412, 0.0), (-0.3876999318599701, 0.10388384014368057, 0.0), (-0.40137654542922974, 3.030309159157696e-08, 0.0), (-0.3876999318599701, -0.10388379544019699, 0.0), (-0.34760230779647827, -0.20068813860416412, 0.0), (-0.2838161289691925, -0.2838158905506134, 0.0), (-0.20068827271461487, -0.34760206937789917, 0.0), (-0.10388395935297012, -0.3876999318599701, 0.0), (-1.5630175198566576e-07, -0.40137654542922974, 0.0), (0.10388367623090744, -0.3876999318599701, 0.0), (0.20068803429603577, -0.34760230779647827, 0.0), (0.2838158905506134, -0.2838161289691925, 0.0), (0.34760206937789917, -0.20068837702274323, 0.0), (0.387699693441391, -0.10388403385877609, 0.0), (0.40137654542922974, -1.8660485068267008e-07, 0.0), (0.3876999318599701, 0.10388367623090744, 0.0), (0.34760230779647827, 0.20068803429603577, 0.0), (0.2838161289691925, 0.2838158905506134, 0.0), (0.20068851113319397, 0.34760206937789917, 0.0), (0.10388415306806564, 0.387699693441391, 0.0), (0.0, 2.990487502074757e-08, 0.40137654542922974), (-0.10388384014368057, 2.990487502074757e-08, 0.3876999318599701), (-0.20068827271461487, 2.990487502074757e-08, 0.34760206937789917), (-0.2838158905506134, 2.990487502074757e-08, 0.2838158905506134), (-0.34760230779647827, 1.4952437510373784e-08, 0.20068813860416412), (-0.3876999318599701, 7.476218755186892e-09, 0.10388384014368057), (-0.40137654542922974, 3.564938905328222e-15, 3.030309159157696e-08), (-0.3876999318599701, -7.476218755186892e-09, -0.10388379544019699), (-0.34760230779647827, -1.4952437510373784e-08, -0.20068813860416412), (-0.2838161289691925, -2.990487502074757e-08, -0.2838158905506134), (-0.20068827271461487, -2.990487502074757e-08, -0.34760206937789917), (-0.10388395935297012, -2.990487502074757e-08, -0.3876999318599701), (-1.5630175198566576e-07, -2.990487502074757e-08, -0.40137654542922974), (0.10388367623090744, -2.990487502074757e-08, -0.3876999318599701), (0.20068803429603577, -2.990487502074757e-08, -0.34760230779647827), (0.2838158905506134, -2.990487502074757e-08, -0.2838161289691925), (0.34760206937789917, -1.4952437510373784e-08, -0.20068837702274323), (0.387699693441391, -7.476218755186892e-09, -0.10388403385877609), (0.40137654542922974, -1.425975562131289e-14, -1.8660485068267008e-07), (0.3876999318599701, 7.476218755186892e-09, 0.10388367623090744), (0.34760230779647827, 1.4952437510373784e-08, 0.20068803429603577), (0.2838161289691925, 2.990487502074757e-08, 0.2838158905506134), (0.20068851113319397, 2.990487502074757e-08, 0.34760206937789917), (0.10388415306806564, 2.990487502074757e-08, 0.387699693441391), (-2.990487502074757e-08, 1.782469452664111e-15, 0.40137654542922974), (-3.738109199957762e-08, -0.10388384014368057, 0.3876999318599701), (-4.485731253112135e-08, -0.20068827271461487, 0.34760206937789917), (-5.980975004149514e-08, -0.2838158905506134, 0.2838158905506134), (-2.990487502074757e-08, -0.34760230779647827, 0.20068813860416412), (-2.990487502074757e-08, -0.3876999318599701, 0.10388384014368057), (-2.990487502074757e-08, -0.40137654542922974, 3.030309159157696e-08), (-2.990487502074757e-08, -0.3876999318599701, -0.10388379544019699), (0.0, -0.34760230779647827, -0.20068813860416412), (0.0, -0.2838161289691925, -0.2838158905506134), (1.4952437510373784e-08, -0.20068827271461487, -0.34760206937789917), (2.2428656265560676e-08, -0.10388395935297012, -0.3876999318599701), (2.990486080989285e-08, -1.5630175198566576e-07, -0.40137654542922974), (3.738109199957762e-08, 0.10388367623090744, -0.3876999318599701), (4.485731253112135e-08, 0.20068803429603577, -0.34760230779647827), (5.980975004149514e-08, 0.2838158905506134, -0.2838161289691925), (2.990487502074757e-08, 0.34760206937789917, -0.20068837702274323), (2.990487502074757e-08, 0.387699693441391, -0.10388403385877609), (2.990487502074757e-08, 0.40137654542922974, -1.8660485068267008e-07), (2.990487502074757e-08, 0.3876999318599701, 0.10388367623090744), (0.0, 0.34760230779647827, 0.20068803429603577), (0.0, 0.2838161289691925, 0.2838158905506134), (-1.4952437510373784e-08, 0.20068851113319397, 0.34760206937789917), (-2.2428656265560676e-08, 0.10388415306806564, 0.387699693441391)) selected_coords = [(coords+Vector(vert)*size*2.5) for vert in vertices] indices = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10], [10, 11], [11, 12], [12, 13], [13, 14], [14, 15], [15, 16], [16, 17], [17, 18], [18, 19], [19, 20], [20, 21], [21, 22], [22, 23], [0, 23], [24, 25], [25, 26], [26, 27], [27, 28], [28, 29], [29, 30], [30, 31], [31, 32], [32, 33], [33, 34], [34, 35], [35, 36], [36, 37], [37, 38], [38, 39], [39, 40], [40, 41], [41, 42], [42, 43], [43, 44], [44, 45], [45, 46], [46, 47], [24, 47], [48, 49], [49, 50], [50, 51], [51, 52], [52, 53], [53, 54], [54, 55], [55, 56], [56, 57], [57, 58], [58, 59], [59, 60], [60, 61], [61, 62], [62, 63], [63, 64], [64, 65], [65, 66], [66, 67], [67, 68], [68, 69], [69, 70], [70, 71], [48, 71]] return selected_coords, indices def serialize_dict(data): """Convert the dictionary to a JSON string, encoding tuple keys as strings.""" converted_data = {str(key): value for key, value in data.items()} return json.dumps(converted_data) def deserialize_dict(serialized): """Convert the JSON string back into a dictionary, decoding tuple keys.""" converted_data = json.loads(serialized) return {eval(key): value for key, value in converted_data.items()} # @persistent def mp_value_update(scene, depsgraph): '''dependency handler to use to avoid modal operator repeating on every mouse move, updating only on action change''' global anim_update global layers_update # Check if any animation data was updated for update in depsgraph.updates: # Check if this update is specifically about animation data if hasattr(update.id, 'bl_rna'): # Check for Action updates if isinstance(update.id, bpy.types.Action): anim_update = True if hasattr(update.id, 'animation_data') and update.id.animation_data: if update.id.animation_data.nla_tracks: layers_update = True def mp_frame_change(self, context): '''dependency handler to use to avoid modal operator repeating on every mouse move''' global frame_change frame_change = True def mp_redo_update(self, context): '''dependency handler to use during redo''' global undo undo = True def mp_undo_update(self, context): '''dependency handler to use during undo''' global undo undo = True def mp_undo_check(self, context): global undo if not undo: return False undo = False # print('undo before deserialization', context.scene.animtoolbox.selected_keyframes) if len(context.scene.emp.selected_keyframes): self.bones_selected_keyframes = deserialize_dict(context.scene.emp.selected_keyframes) else: self.bones_selected_keyframes = {} if not self.mp_bone_names: return False #get all the items of the motion path using the bone or object names, since it's getting broken when using undo get_mp_items(self) get_mp_points(self, context) update_keyframe_coords(self) calculate_velocities(self, context) self.update = True return True def remove_draw_handlers(self): '''Remove motion path drawing and frames drawing''' if hasattr(self, '_handle'): bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') del self._handle if hasattr(self, 'draw_frames'): bpy.types.SpaceView3D.draw_handler_remove(self.draw_frames, 'WINDOW') del self.draw_frames if 'mp_dh' in bpy.app.driver_namespace: bpy.app.driver_namespace.pop('mp_dh') if 'mp_df' in bpy.app.driver_namespace: bpy.app.driver_namespace.pop('mp_df') self.mp_vis = False # Operator with modal and draw handler class MotionPathOperator(bpy.types.Operator): """Creates a custom motion path""" bl_idname = "object.motion_path_operator" bl_label = "Motion Path Operator" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): if context.scene.emp.motion_path: return True return True if context.selected_objects else False # return len(context.selected_pose_bones) @property def range_value(self): #the range distance around the keyframes for hovering, getting a dynamic value return bpy.context.preferences.addons[__package__].preferences.keyframes_range def invoke(self, context, event): scene = context.scene atb = scene.animtoolbox emp = scene.emp if emp.motion_path: emp.motion_path = False return {'CANCELLED'} if context.selected_pose_bones is None and context.selected_objects is None: emp.motion_path = False return {'CANCELLED'} emp.motion_path = True global undo, handle_type, interpolation undo = False handle_type = None interpolation = None bpy.app.handlers.depsgraph_update_post.append(mp_value_update) # bpy.app.handlers.frame_change_post.append(mp_frame_change) bpy.app.handlers.undo_pre.append(mp_undo_update) bpy.app.handlers.redo_pre.append(mp_redo_update) self.window = context.window self.show_overlays = True #Check if we use motion path in pose mode or object mode self.posemode = True if context.selected_pose_bones else False #Notify when it's turned off because of overlay or object mode self.mp_vis = check_mp_vis_off(self, context) self.press = False #boolean turned on while pressing and moving a keyframe self.shift = False self.ctrl = False self.scale = False self.rotate = False self.filter_axis = '' # Draw only during update self.update = False # Using this variable to update the draw only once during hover self.current_hover_frame = (None, None) #check for right or left mouse selection preferences = context.window_manager.keyconfigs.default.preferences if preferences: self.select_mouse = preferences.select_mouse + 'MOUSE' self.cancel_mouse = 'RIGHTMOUSE' if self.select_mouse == 'LEFTMOUSE' else 'LEFTMOUSE' else: self.select_mouse, self.cancel_mouse = 'LEFTMOUSE', 'RIGHTMOUSE' self.hover_bone_frame = {'keyframe': None, 'handle_r' : None, 'handle_l' : None} #get the bone and frame of the keyframe that change color while hovering # self.hover_handle = {'r' : None, 'l' : None} #get the bone and frame of the handle that change color while hovering self.prev_event_value = None # records the previous mouse event to see if it was just a click and release for selecting # self.moving_frame = dict() #frame number of the keyframe that is selected and being moved self.initial_coord = None #the starting point of the coords when starting to move them with g or mouse press self.prev_view_matrix = context.region_data.view_matrix.copy() #caluclating 2d coordinates only when view and window matrixchange self.prev_window_matrix = context.region_data.window_matrix.copy() # self.range_value = 15 #the range distance around the keyframes for hovering # self.timer_tick_counter = 0 #using a timer while motion paths are updated # self.timer = None self.items = context.selected_pose_bones if self.posemode else context.selected_objects #get rid of the items that don't have animation data self.items = [item for item in self.items if item.id_data.animation_data] if not self.items: emp.motion_path = False return {'CANCELLED'} self.frame_range_type = emp.frame_range self.use_preview_range = scene.use_preview_range self.frame_start = dict() self.frame_end = dict() # extra start property incase we need to calculate the path before the official start # if frame start is between two keyframes, then it will start from the previous keyframe self.avz_frame_start = dict() #All the points of the motion path per bone self.bones_points = dict() #All the coordinates of the keyframes per bone, it has a sub dictionary of keyframes_coords with the frame number and it's coordinates self.bones_keyframes_coords = dict() #a copy of the keyframe coords before they start to move self.initial_keyframe_coords = None #check mark which bones are cyclic self.bones_cyclic = dict() #coordinates for all the frames when using the coordinates self.bones_allframes_coords = dict() #The 2D coordinates of the keyframes per bone, , it has a sub dictionary of keyframes_coords with the frame number and it's coordinates self.coords2d_bones_frames = dict() #The 2D coordinates of the keyframes per bone, , it has a sub dictionary of handles coords with the frame number and it's coordinates self.coords2d_handles_r = dict() self.coords2d_handles_l = dict() self.coords2d_ranges = [] #Dictionary of selected bones and the frame numbers of the selected keyframes self.bones_selected_keyframes = dict() self.bones_selected_handles_r = dict() self.bones_selected_handles_l = dict() #dictinary of bones, frames and the handles coordinates self.bones_handles_right = dict() self.bones_handles_left = dict() self.mp_handles = emp.handles #dictinary of bones, keyframe frames and handles frames self.bones_handles_frames = dict() #dictinary of bones, keyframe frames and handles types self.bones_handles_types = dict() #dictinary of bones, handles frames and the handles coordinates # self.bones_frames_handles = dict() self.mp_bones_keys = dict() self.mp_bone_names = [] # Setting up the dictionaries for the shader containers self.batch_line = dict() self.batch_points = dict() self.batch_keyframes = dict() self.batch_selected = dict() # self.batch_handles_lines = dict() # self.batch_handles_points = dict() nfr = context.preferences.edit.use_negative_frames for item in self.items: obj_bonename = (item.id_data.name, item.name) self.mp_bone_names.append(obj_bonename) obj = item.id_data if self.posemode: avz_mp = obj.pose.animation_visualization.motion_path else: avz_mp = obj.animation_visualization.motion_path avz_mp.range ='KEYS_ALL' if emp.frame_range == 'AROUND' else emp.frame_range #Camera space relevant only from version 4.1 - currently disabled if hasattr(avz_mp, 'bake_in_camera_space'): if avz_mp.bake_in_camera_space: avz_mp.bake_in_camera_space = False #assign the frame range per object frame_start, frame_end = get_frame_range(scene, obj) self.frame_start.update({obj_bonename : frame_start}) self.frame_end.update({obj_bonename : frame_end}) update_avz_frame_start(self, context, obj_bonename) #Negative frame range, needs to be checked in case frame start is minus turn_nfr_on(context.preferences, self.avz_frame_start[obj_bonename]) avz_mp.frame_start, avz_mp.frame_end = self.avz_frame_start[obj_bonename], frame_end #store bone names in case they were changed item['atb_mp_name'] = item.name item.id_data['atb_mp_name'] = item.id_data.name self.current_selected_items = context.selected_pose_bones if self.posemode else context.selected_objects self.frame_current = scene.frame_current_final matrix_differece(self, context) self.current_bones_keys = get_current_bones_keys(self) self.obj_layer_properties = get_layer_properties(self.items) global anim_update, frame_range_update, names_update, layers_update anim_update = True layers_update = False frame_range_update = False names_update = False subscribe_to_names(emp) if emp.frame_range == 'SCENE': subscribe_to_scene_frame_range(scene) # update_scene_frame_range() get_mp_points(self, context) update_keyframe_coords(self) if emp.vis_type == 'VELOCITY': global velocity_update velocity_update = False calculate_velocities(self, context) #return negative frame range to what it was if context.preferences.edit.use_negative_frames != nfr: context.preferences.edit.use_negative_frames = nfr #assign the draw handler context.window_manager.modal_handler_add(self) #If overlays are turned off then skip drawing if self.mp_vis: self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_motionpath_callback_px, (self, context,), 'WINDOW', 'POST_VIEW') dns = bpy.app.driver_namespace dns['mp_dh'] = self._handle if emp.display_frames: self.draw_frames = bpy.types.SpaceView3D.draw_handler_add(draw_frames_callback_px, (self, context,), 'WINDOW', 'POST_PIXEL') dns['mp_df'] = self.draw_frames coords_2d_update(self, context) self.update = True context.area.tag_redraw() # Tools.redraw_areas(['VIEW_3D']) bpy.ops.ed.undo_push(message = 'Initialize Motion Path') return {'RUNNING_MODAL'} def modal(self, context, event): # using a timer when creating a motion path so that the hotkey G doesnt respond before the path is created # if event.type == 'TIMER' and self.timer: # if self.timer_tick_counter < 3: # self.timer_tick_counter += 1 # # print('timer counter') # return {'RUNNING_MODAL'} # else: # context.window_manager.event_timer_remove(self.timer) # self.timer_tick_counter = 0 # self.timer = None #quit the modal operators # if event.type in {'ESC'}: # context.scene.animtoolbox.motion_path = False emp = context.scene.emp #Quit the draw handler if not emp.motion_path: quit_mp(self, context) return {'FINISHED'} if not self.mp_vis: check_mp_vis_on(self, context) return {'PASS_THROUGH'} try: global anim_update if mp_undo_check(self, context): return {'PASS_THROUGH'} if check_bone_names(self, context): return {'PASS_THROUGH'} self.info(context, event) region = context.region self.mouse_x = event.mouse_x - region.x self.mouse_y = event.mouse_y - region.y if compare_bone_keys(self,context, event): return {'PASS_THROUGH'} compare_obj_layers(self, context, event) if mp_frame_range_change(self, context): return {'PASS_THROUGH'} if check_velocity_update(self, context): return {'PASS_THROUGH'} #in case the draw handler stopped when the armature went out of edit mode dns = bpy.app.driver_namespace if not hasattr(self, '_handle'): self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_motionpath_callback_px, (self, context,), 'WINDOW', 'POST_VIEW') dns['mp_dh'] = self._handle self.update = True context.area.tag_redraw() #Add or remove frame numbers depending on the options if emp.display_frames and not hasattr(self, 'draw_frames'): self.draw_frames = bpy.types.SpaceView3D.draw_handler_add(draw_frames_callback_px, (self, context,), 'WINDOW', 'POST_PIXEL') dns['mp_df'] = self.draw_frames if not emp.display_frames and hasattr(self, 'draw_frames'): if hasattr(self, 'draw_frames'): bpy.types.SpaceView3D.draw_handler_remove(self.draw_frames, 'WINDOW') del self.draw_frames if 'mp_df' in dns: dns.pop('mp_df') if not check_mp_vis_off(self, context): self.mp_vis = False remove_draw_handlers(self) context.area.tag_redraw() return{'PASS_THROUGH'} if event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'} and event.value == 'PRESS': self.shift = True elif event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'} and event.value == 'RELEASE': self.shift = False if zoom_keyframes(self, context, event): return {'RUNNING_MODAL'} if edit_motion_path_update(self, context, event): return {'RUNNING_MODAL'} if self.press or self.scale or self.rotate: return {'RUNNING_MODAL'} #check if the bone selection was changed selected_items = context.selected_pose_bones if self.posemode else context.selected_objects if self.current_selected_items != selected_items: #need to checek this condition if selected_items not in self.current_selected_items: self.current_selected_items = selected_items self.current_bones_keys = get_current_bones_keys(self) return {'PASS_THROUGH'} self.current_selected_items = context.selected_pose_bones return {'PASS_THROUGH'} anim_update = False mp_handles_on_off(self, context) mp_set_handle_type(self, context, event) mp_set_interpolation(self, context, event) return {'PASS_THROUGH'} except Exception as e: # Log the error print("Error:", e) context.scene.emp.motion_path = False quit_mp(self, context) self.report({'ERROR'}, str(e) + '. Quitting Motion Paths') traceback.print_exc() return {'CANCELLED'} def info(self, context, event): def draw_status(drawself, context): row = drawself.layout.row(align=True) if any((self.press, self.rotate, self.scale)): row.label(text=" or",icon="EVENT_ESC") row.label(text=": Cancel operation", icon="MOUSE_RMB") row.label(text="", icon="EVENT_X") row.label(text="", icon="EVENT_Y") row.label(text=": Lock an axis", icon="EVENT_Z") # row.label(text="X/Y/Z to lock an axis") else: row.label(text="Hotkeys during keyframe hovering ") row.label(text=": Move/Select Keyframes", icon="MOUSE_LMB") row.separator() row.label(icon="EVENT_G") row.label(icon="EVENT_R") row.label(text=": Move/Rotate/Scale ", icon="EVENT_S") row.separator() row.label(text=": Add/Remove to selection", icon="KEY_SHIFT") row.separator() row.label(text=" +", icon="EVENT_CTRL") row.label(text=": Add/Remove Keyframes", icon="MOUSE_LMB") row.separator() row.label(text=" +", icon="EVENT_CTRL") row.label(text=": Set Interpolation", icon="EVENT_T") row.separator() row.label(text=": Set Handles Type", icon="EVENT_V") row.separator() row.label(text=": Zoom Keyframes", icon="EVENT_PERIOD") context.workspace.status_text_set(draw_status) def check_mp_vis_on(self, context): '''Check if the visiblity turned back on after it was off because of overlays or posemode''' if context.mode != 'POSE' and self.posemode: return if hasattr(context.space_data, 'overlay'): if not context.space_data.overlay.show_overlays and not self.show_overlays: return if not context.space_data.overlay.show_motion_paths and not self.show_overlays: return #Turn Motion Path visibilty back on self.mp_vis = True def check_mp_vis_off(self, context): '''Check if the visibility need to turn off''' #when going out of pose mode remove the draw handler, bone motion path is hidden if context.mode != 'POSE' and self.posemode: return False if hasattr(context.space_data, 'overlay'): if not context.space_data.overlay.show_overlays and self.show_overlays: self.show_overlays = context.space_data.overlay.show_overlays return False #In case motion path is specifically turned off from overlays elif not context.space_data.overlay.show_motion_paths and self.show_overlays: self.show_overlays = context.space_data.overlay.show_motion_paths return False elif self.show_overlays != context.space_data.overlay.show_overlays: self.show_overlays = context.space_data.overlay.show_overlays return True def quit_mp(self, context): remove_draw_handlers(self) # for obj, self.mp_bone_names in self.mp_obj_bone_names.items(): for obj_bonename in self.mp_bone_names: objname, bonename = obj_bonename if objname not in context.view_layer.objects: print(objname, 'not found in objects') continue obj = context.view_layer.objects[objname] if 'atb_mp_name' in obj.keys(): del obj['atb_mp_name'] if not self.posemode: continue bone = obj.pose.bones[bonename] if 'atb_mp_name' in bone.keys(): del bone['atb_mp_name'] #remove dependecy handler bpy.app.handlers.depsgraph_update_post.remove(mp_value_update) # bpy.app.handlers.frame_change_post.remove(mp_frame_change) bpy.app.handlers.redo_pre.remove(mp_redo_update) bpy.app.handlers.undo_pre.remove(mp_undo_update) clear_frame_range_owner() clear_names_owner() # Clear the status bar info before finishing context.workspace.status_text_set(None) context.scene.emp.selected_keyframes = '{}' remove_global_variables() Tools.redraw_areas(['VIEW_3D']) def remove_global_variables(): global undo, anim_update, frame_range_update, names_update, layers_update, velocity_update for var in ["undo", "anim_update", "frame_range_update", "names_update", "layers_update", "velocity_update"]: if var in globals(): del globals()[var] def mp_set_interpolation(self, context, event): global interpolation if interpolation: frames = update_mp_handle_types(self, context, interpolation) #remove handles that are not bezier anymore if interpolation != 'BEZIER': for obj_bonename in self.mp_bone_names: for frame in frames: handles_frame_remove(self, obj_bonename, frame) update_mp_points(self, context, frames) calculate_velocities(self, context) #get current bones keys to avoid recalculation of path points self.current_bones_keys = get_current_bones_keys(self) interpolation = None if event.type != 'T' or event.value != 'PRESS' or not self.ctrl: return self.ctrl = False for win in context.window_manager.windows: for area in win.screen.areas: if area.type != 'VIEW_3D': continue if area.width > event.mouse_x > area.x and area.y + area.height > event.mouse_y > area.y: context.window_manager.popup_menu(draw_interpolation_menu, title = 'Set Interpolation') return def mp_set_handle_type(self, context, event): global handle_type if handle_type: frames = update_mp_handle_types(self, context, handle_type) if not frames: return update_mp_points(self, context, frames) calculate_velocities(self, context) #get current bones keys to avoid recalculation of path points self.current_bones_keys = get_current_bones_keys(self) handle_type = None if event.type != 'V' or event.value != 'PRESS': return if not self.bones_selected_keyframes: return for win in context.window_manager.windows: for area in win.screen.areas: if area.type != 'VIEW_3D': continue if area.width > event.mouse_x > area.x and area.y + area.height > event.mouse_y > area.y: context.window_manager.popup_menu(draw_handle_type_menu, title = 'Set Keyframe Handle Type') return def draw_handle_type_menu(self, context): '''Draw the menu for the handle types of the selected keyframes''' layout = self.layout layout.prop(context.scene.emp, 'handle_types', expand = True) def draw_interpolation_menu(self, context): layout = self.layout layout.prop(context.scene.emp, 'interpolation', expand = True) def update_handle_type_prop(self, context): '''updating handle type when the property is being selected''' global handle_type handle_type = self.handle_types def update_interpolation_prop(self, context): global interpolation interpolation = self.mp_interpolation def turn_nfr_on(preferences, start): '''Motion path doesnt draw negative frames if it's turned off and we are using MANUAL''' edit = preferences.edit nfr = edit.use_negative_frames #if it's already turned on if start >= 0 or nfr: return nfr edit.use_negative_frames = True return nfr def get_frame_range(scene, obj): '''Get the frame range values depending on the frame range type''' emp = scene.emp if emp.frame_range == 'SCENE': frame_start = scene.frame_start if not scene.use_preview_range else scene.frame_preview_start frame_end = scene.frame_end if not scene.use_preview_range else scene.frame_preview_end frame_end = version_frame_end(frame_end) elif emp.frame_range == 'KEYS_ALL': frame_start = int(obj.animation_data.action.frame_range[0]) frame_end = int(obj.animation_data.action.frame_range[1]) elif emp.frame_range == 'MANUAL': frame_start = emp.frame_start frame_end = emp.frame_end elif emp.frame_range == 'AROUND': # scene_start = scene.frame_start if not scene.use_preview_range else scene.frame_preview_start # scene_end = scene.frame_end if not scene.use_preview_range else scene.frame_preview_end keys_start = int(obj.animation_data.action.frame_range[0]) keys_end = int(obj.animation_data.action.frame_range[1]) frame_start = keys_start # max([scene_start, keys_start]) frame_end = keys_end # min([scene_end, keys_end]) return frame_start, frame_end def update_frame_range_type(self, context): '''Updating depends on the frame range selected''' scene = context.scene emp = context.scene.emp # If motion path is not turned on then skip it if not emp.motion_path: return clear_frame_range_owner() global frame_range_update if self.frame_range == 'MANUAL': update_manual_frame_range(self, context) return # When using scene frame range then subscribe to the scene frame range if self.frame_range == 'SCENE': subscribe_to_scene_frame_range(scene) else: frame_range_update = True def update_manual_frame_range(self, context): if self.frame_start > self.frame_end: self.frame_start = self.frame_end if self.frame_start > self.frame_end: self.frame_end = self.frame_start # In the first time it's activated frame end has value of 0 and it will get the scene frame range scene = context.scene if not self.frame_end: self['frame_start'] = scene.frame_start if not scene.use_preview_range else scene.frame_preview_start self['frame_end'] = scene.frame_end if not scene.use_preview_range else scene.frame_preview_end global frame_range_update frame_range_update = True def update_scene_frame_range(): global frame_range_update frame_range_update = True def clear_frame_range_owner(): global frame_range_owner if 'frame_range_owner' in globals(): bpy.msgbus.clear_by_owner(frame_range_owner) del frame_range_owner def subscribe_to_scene_frame_range(scene): '''subscribe_to_frame_end and frame preview end''' global frame_range_owner frame_range_owner = object() subscriptions = {"frame_start", "frame_end", "frame_preview_start", "frame_preview_end","use_preview_range" } for subscribe in subscriptions: bpy.msgbus.subscribe_rna( key=scene.path_resolve(subscribe, False), owner=frame_range_owner, args=(), notify=update_scene_frame_range,) update_scene_frame_range() def mp_frame_range_change(self, context): '''Updating the motion path when changing the frame range''' global frame_range_update if not frame_range_update: return False frame_range_update = False scene = context.scene emp = scene.emp nfr = context.preferences.edit.use_negative_frames frames = [] #update all the frame ranges and types for obj_bonename in self.mp_bone_names: objname, bonename = obj_bonename obj = context.view_layer.objects[objname] avz_mp = obj.pose.animation_visualization.motion_path if self.posemode else obj.animation_visualization.motion_path frame_start, frame_end = get_frame_range(scene, obj) #Negative frame range, needs to be checked in case frame start is minus prev_frame_start = self.avz_frame_start.get(obj_bonename)#[obj_bonename] prev_frame_end = self.frame_end[obj_bonename] #apply the property to all the settings if (frame_start != self.frame_start[obj_bonename] or frame_end != self.frame_end[obj_bonename]): self.frame_start[obj_bonename] = frame_start # = emp['frame_start'] self.frame_end[obj_bonename] = frame_end # = emp['frame_end'] self.frame_range_type = emp.frame_range #= avz_mp.range self.use_preview_range = scene.use_preview_range update_avz_frame_start(self, context, obj_bonename) turn_nfr_on(context.preferences, self.avz_frame_start[obj_bonename]) avz_mp.frame_start = frame_start avz_mp.frame_end = frame_end if obj_bonename not in self.bones_points: continue #update the begining of the motion path if self.avz_frame_start[obj_bonename] != prev_frame_start: if self.avz_frame_start[obj_bonename] > prev_frame_start: remove_points = (self.avz_frame_start[obj_bonename] - prev_frame_start)*3 self.bones_points[obj_bonename] = self.bones_points[obj_bonename][remove_points:] elif self.avz_frame_start[obj_bonename] < prev_frame_start: extra_points = np.zeros(abs(prev_frame_start - self.avz_frame_start[obj_bonename])*3) self.bones_points[obj_bonename] = np.concatenate((extra_points, self.bones_points[obj_bonename])) frames += list(range(self.avz_frame_start[obj_bonename], (prev_frame_start))) #update the end of the motion path if self.frame_end[obj_bonename] != prev_frame_end: if self.frame_end[obj_bonename] < prev_frame_end: remove_points = (prev_frame_end - self.frame_end[obj_bonename])*3 self.bones_points[obj_bonename] = self.bones_points[obj_bonename][:-remove_points] elif self.frame_end[obj_bonename] > prev_frame_end: extra_frames = self.frame_end[obj_bonename] - prev_frame_end extra_points = np.zeros(abs(extra_frames)*3) self.bones_points[obj_bonename] = np.concatenate((self.bones_points[obj_bonename], extra_points)) frames += (range(prev_frame_end, self.frame_end[obj_bonename])) if context.preferences.edit.use_negative_frames != nfr: context.preferences.edit.use_negative_frames = nfr if frames: update_mp_points(self, context, frames, fr_update = True) update_keyframe_coords(self) calculate_velocities(self, context) # print('updating points', self.bones_points[obj_bonename]) self.update = True return True def version_frame_end(frame_end): '''Frame end for motion paths changed from version 4.1 in one frame''' if bpy.app.version < (4, 1, 0): frame_end += 1 return frame_end def update_mp_bones_keys(self, item, obj_bonename): frames_with_offset = map(lambda frame: round(add_frame_offset(item.id_data, frame), 2), get_bone_keyframes(item)[::2]) frames_with_offset = sorted(set(frames_with_offset)) self.mp_bones_keys.update({obj_bonename : frames_with_offset}) def update_avz_frame_start(self, context, obj_bonename): '''an extra start property in case the motion path starts between keyframes and is cyclic I need to calculate everything coming from the keyframe''' objname, bonename = obj_bonename obj = context.view_layer.objects[objname] item = obj.pose.bones[bonename] if self.posemode else obj # if obj_bonename in self.mp_bones_keys: # frames = sorted(set(self.mp_bones_keys[obj_bonename])) # else: update_mp_bones_keys(self, item, obj_bonename) # update_keyframe_coords(self) frames = self.mp_bones_keys[obj_bonename] frame_start = self.frame_start[obj_bonename] #in case there is no keyframes if not frames: self.avz_frame_start.update({obj_bonename: frame_start}) return if self.frame_start[obj_bonename] in frames: self.avz_frame_start.update({obj_bonename: frame_start}) return i = bisect.bisect_left(frames, frame_start) avz_frame_start = round(frames[i-1]) if i else round(frames[i]) #this should apply only when it's smaller then the actual frame start if avz_frame_start > frame_start: avz_frame_start = frame_start self.avz_frame_start.update({obj_bonename: avz_frame_start}) def frame_range_reset(self, context): ''' Get the original frame range back to Blender's motion path ''' obj_bonenames = {obj_bonename for obj_bonename in self.mp_bones_keys.keys()} for obj_bone in obj_bonenames: obj = context.view_layer.objects[obj_bone[0]] avz_mp = obj.pose.animation_visualization.motion_path if self.posemode else obj.animation_visualization.motion_path avz_mp.frame_start = self.avz_frame_start[obj_bone] avz_mp.frame_end = self.frame_end[obj_bone] def available_for_update(modifiers): '''check if the values of the modifiers are equal''' if not len(modifiers): return True if len(modifiers) < 3: return False props = ['cycles_after', 'cycles_before', 'frame_end', 'frame_start', 'mode_after', 'mode_before','use_restricted_range'] #list of all the dictionaries mod_dicts = [] for mod in modifiers: mod_props = dict() for prop in props: value = getattr(mod, prop) mod_props.update({prop : value}) mod_dicts.append(mod_props) if mod_dicts[0] == mod_dicts[1] == mod_dicts[2]: return True else: return False def restrict_range(start_cyclic, end_cyclic, restrict_start_point, restrict_end_point): if start_cyclic < restrict_start_point: start_cyclic = restrict_start_point if end_cyclic > restrict_end_point: end_cyclic = restrict_end_point if end_cyclic < restrict_start_point or restrict_start_point > restrict_end_point: start_cyclic = end_cyclic = 0 return start_cyclic, end_cyclic def repeat_mp_difference(self, obj_bonename, repeat, difference, length, start, end, mod): for r in range(1, repeat + 1): #the index of the points where the values are being added start_repeat = start + length * r *3 end_repeat = end + length * r * 3 if end_repeat < 0: continue if start_repeat < 0: #the cyclic starts before 0, reseting to 0 and slicing it from the difference difference = difference[-start_repeat:] start_repeat = 0 # continue diff_add = difference # Ensure the frame range is within the restriced range if mod.use_restricted_range: frame_start_repeat = ((start_repeat+3) / 3) + self.avz_frame_start[obj_bonename] - 1 frame_end_repeat = ((end_repeat+3) / 3) + self.avz_frame_start[obj_bonename] - 1 adjusted_start = max(frame_start_repeat, mod.frame_start) adjusted_end = min(frame_end_repeat, mod.frame_end + 1) if not mod.frame_start < adjusted_start < mod.frame_end and not mod.frame_start < adjusted_end < mod.frame_end: continue # adding the restricted frames to the frame range start_adjust = round((adjusted_start - frame_start_repeat)*3) end_adjust = round((frame_end_repeat - adjusted_end)*3) start_repeat = int(round(start_repeat + start_adjust)) end_repeat = int(round(end_repeat - end_adjust)) #matching the difference array length to the restricted frame range diff_add = difference[max(0, start_adjust) : max(0, len(diff_add) - end_adjust)] if len(self.bones_points[obj_bonename][start_repeat : end_repeat]) != len(diff_add): #in case the cyclic is not complete and the last repeat is missing some frames from the difference array i = len(self.bones_points[obj_bonename][start_repeat : end_repeat]) - len(diff_add) diff_add = diff_add[:i] # Debug logging # print(f"Repeat #{r}: start={start_repeat}, end={end_repeat}, diff_add={len(diff_add)} end-start {end_repeat - start_repeat}") self.bones_points[obj_bonename][start_repeat : end_repeat] += diff_add def update_mp_points(self, context, frames, included = None, key = None, coord_3d = None, fr_update = False): '''change the frame range for the motion path to update only specific points''' frame_range = [] bones_cycles = dict() def get_i_keyframe_and_modifiers(): #get all the keyframes from all the arrays and check what is the last previous and next value fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action) path = get_location_path(self.posemode, obj, bonename) #get all the cyclic modifiers for i in range(3): fcu = fcurves.find(data_path = path, index = i) if fcu is None: continue if len(fcu.modifiers): for mod in fcu.modifiers: if mod.type != 'CYCLES' or mod.mute: continue #Influence works in a strange way, recalcuating the whole path in this case if mod.use_influence and mod.influence < 1: get_mp_points(self, context) return #Blend mode works in a strange way, recalcuating the whole path in this case if mod.use_restricted_range: if mod.blend_in or mod.blend_out: get_mp_points(self, context) return #Currently including only none and repeat if mod.mode_before not in {'NONE', 'REPEAT'} or mod.mode_after not in {'NONE', 'REPEAT'}: get_mp_points(self, context) return modifiers.append(mod) if not len(fcu.keyframe_points): continue keyframes = np.zeros(len(fcu.keyframe_points)*2) # keyframes = [0]* (len(fcu.keyframe_points)*2) fcu.keyframe_points.foreach_get('co', keyframes) #get the frames with the frame offset keyframes = list(map(lambda frame: add_frame_offset(obj, frame), keyframes[::2])) i_keyframes.update({i : keyframes}) if modifiers: bones_cycles.update({obj_bonename : modifiers}) def restore_points_before_cyclic(): # Moving points back to the initial coords before adding cyclic if key != 'keyframe': return if obj_bonename not in bones_cycles: return distance = coord_3d - self.initial_coord frames_distance = dict() for frame in self.bones_selected_keyframes[obj_bonename]: restore_point = (round(frame - self.avz_frame_start[obj_bonename])) *3 # if obj_bonename in bones_cycles: #restoring previous points before repeating them in cyclic fcurves back to the initial coordinates self.bones_points[obj_bonename][restore_point : restore_point+3] -= distance frames_distance.update({frame : distance * -1}) add_distance_to_points(self, obj_bonename, frames_distance) def get_prev_next_keyframes(frame_range): # Checking for the next and previous keyframes to get the frame range that needs to be updated for keyframes in i_keyframes.values(): keyframes = np.round(keyframes, 2) for frame in frames: prev_i = bisect.bisect_left(keyframes, frame) if prev_i: prev_i -= 1 prev_keyframe = keyframes[prev_i] #get frames that are pre first keyframe if frame < keyframes[0]: prev_keyframe = frame next_i = bisect.bisect_right(keyframes, frame) if next_i >= len(keyframes): #get frames that are after the last keyframe next_keyframe = frame else: next_keyframe = keyframes[next_i] frame_range.append(prev_keyframe) frame_range.append(next_keyframe) return frame_range def add_distance_before_after(self, obj_bonename): if key in {'handle_r', 'handle_l'}: return if obj_bonename not in self.mp_bones_keys: return first = self.mp_bones_keys[obj_bonename][0] last = self.mp_bones_keys[obj_bonename][-1] first_last = [first, last] if first != last else [first] #all the keyframes that has distance restored used for inbetween frames for frame in first_last: if frame not in frames: continue restore_point = (round(frame - self.avz_frame_start[obj_bonename])) *3 coord = self.bones_points[obj_bonename][restore_point : restore_point+3] # initial_coords = stored_bones_points[obj_bonename][restore_point : restore_point+3] if coord is None: continue if self.initial_keyframe_coords is None or obj_bonename not in self.initial_keyframe_coords: continue distance = coord - self.initial_keyframe_coords[obj_bonename][frame] if first == last and restore_point-3: # If there is only one keyframe which is the first and last Blender will still create # A motion path with the previous frame, so it needs to be restored before adding all the start and end points prev_initial = stored_bones_points[obj_bonename][restore_point-3 : restore_point] self.bones_points[obj_bonename][restore_point-3 : restore_point] = prev_initial #Adding the distance to the rest of the points before and after the first keys if frame == first: #adding distance to all frames before the first keyframe length = restore_point // 3 distance_array = np.tile(np.array(distance), round(length)) self.bones_points[obj_bonename][:restore_point] += distance_array if frame == last: #adding distance to all frames after the last keyframe remaining_points = self.bones_points[obj_bonename][restore_point+3:] if len(remaining_points) > 0: length = len(remaining_points)//3 distance_array = np.tile(np.array(distance), round(length)) remaining_points += distance_array for obj_bonename, keys in self.mp_bones_keys.items(): objname, bonename = obj_bonename obj = context.view_layer.objects[objname] avz_mp = obj.pose.animation_visualization.motion_path if self.posemode else obj.animation_visualization.motion_path #Skip cyclic when the update frames are before or after the actual animation (when updating frame range) if fr_update: avz_mp.frame_start = min(frames) avz_mp.frame_end = max(frames) + 1 continue if included: if obj_bonename not in included: continue modifiers = [] i_keyframes = dict() get_i_keyframe_and_modifiers() #if it's only partly cyclic then recalculate the whole motion path if not available_for_update(modifiers): get_mp_points(self, context) return get_prev_next_keyframes(frame_range) #usually breaks when selecting a layer with no keyframes if not frame_range or (min(frame_range), max(frame_range)) == (self.avz_frame_start[obj_bonename], self.frame_end[obj_bonename]): get_mp_points(self, context) return #creating a copy for cyclic points to be used after the distance array was added to the first and last keys cyclic_points = copy.deepcopy(self.bones_points) restore_points_before_cyclic() #Temporarly change the frame range around the modified keyframes temp_start = min(frame_range) temp_end = max(frame_range) #adding one frame to include the last frame in the calculation temp_end = version_frame_end(temp_end) if temp_start < self.avz_frame_start[obj_bonename]: temp_start = self.avz_frame_start[obj_bonename] if temp_end > self.frame_end[obj_bonename]: temp_end = self.frame_end[obj_bonename] avz_mp.frame_start = round(temp_start) avz_mp.frame_end = round(temp_end) stored_bones_points = self.bones_points.copy() get_mp_points(self, context, included) #Recalculating the new points# #Merge new frames with previous frames for obj_bonename in self.mp_bones_keys.keys(): if included: if obj_bonename not in included: continue objname, bonename = obj_bonename obj = context.view_layer.objects[objname] avz_mp = obj.pose.animation_visualization.motion_path if self.posemode else obj.animation_visualization.motion_path #start and end points # Need to add solution for negative points if bpy.app.version < (4, 1, 0): avz_mp.frame_end -= 1 start_p = (avz_mp.frame_start - self.avz_frame_start[obj_bonename]) * 3 end_p = (avz_mp.frame_end - self.avz_frame_start[obj_bonename]) * 3 if bpy.app.version >= (4, 1, 0) : end_p += 3 if end_p > len(stored_bones_points[obj_bonename]): end_p = len(stored_bones_points[obj_bonename]) if self.bones_points[obj_bonename].shape[0] != stored_bones_points[obj_bonename][start_p : end_p].shape[0]: #checking if length is not equal and recalculating the whole path if it's not print(f'length is not equal for {obj_bonename}') print(f'selected objects {context.selected_objects} bones {context.selected_pose_bones}') print(f'self.bones_points {self.bones_points[obj_bonename].shape[0]} stored_bones_points {stored_bones_points[obj_bonename][start_p : end_p].shape[0]}') frame_range_reset(self, context) get_mp_points(self, context, included) return #Get the difference between the old points and the new points to use in cyclic modifier difference = self.bones_points[obj_bonename] - stored_bones_points[obj_bonename][start_p : end_p] restored_start = stored_bones_points[obj_bonename][:start_p] restored_end = stored_bones_points[obj_bonename][end_p:] self.bones_points[obj_bonename] = np.concatenate((restored_start, self.bones_points[obj_bonename], restored_end)) add_distance_before_after(self, obj_bonename) #skip if not cyclic if obj_bonename not in bones_cycles: continue end_key = max(self.mp_bones_keys[obj_bonename]) start_key = min(self.mp_bones_keys[obj_bonename]) length = round(end_key - start_key) if not length: continue mod = bones_cycles[obj_bonename][0] if start_key < self.avz_frame_start[obj_bonename]: start_key = self.avz_frame_start[obj_bonename] repeat_after = math.ceil((self.frame_end[obj_bonename] - end_key)//length) repeat_before = math.ceil((start_key - self.avz_frame_start[obj_bonename])//length) # checking if it starts or finish earlier based on the cyclic settings cycles_before = mod.cycles_before if cycles_before: if repeat_before > cycles_before: repeat_before = cycles_before elif mod.mode_before == 'NONE': repeat_before = 0 cycles_after = mod.cycles_after if cycles_after: if repeat_after > cycles_after: repeat_after = cycles_after elif mod.mode_after == 'NONE': repeat_after = 0 #getting the point for the restrict frame range restrict_start_point = (round(mod.frame_start - self.avz_frame_start[obj_bonename])) *3 restrict_end_point = (round(mod.frame_end - self.avz_frame_start[obj_bonename])) *3 + 3 #Restoring cyclic animation before distance array was added to all the before and after values start_after_cyclic = (round(end_key - self.avz_frame_start[obj_bonename])) *3 end_after_cyclic = round(start_after_cyclic + length * (repeat_after -1 )) *3 + 3 end_before_cyclic = (round(start_key - self.avz_frame_start[obj_bonename])) *3# + 3 start_before_cyclic = round(end_before_cyclic - length * (repeat_before)) * 3 #Add the difference to cyclic before if avz_mp.frame_end != self.frame_end[obj_bonename]: #Apply the restrict range if mod.use_restricted_range: start_after_cyclic, end_after_cyclic = restrict_range(start_after_cyclic, end_after_cyclic, restrict_start_point, restrict_end_point) self.bones_points[obj_bonename][start_after_cyclic : end_after_cyclic] = cyclic_points[obj_bonename][start_after_cyclic : end_after_cyclic] repeat_mp_difference(self, obj_bonename, repeat_after, difference[3:], length, start_p + 3, end_p, mod) #Add the difference to cyclic after if avz_mp.frame_start != self.avz_frame_start[obj_bonename]: #Apply the restrict range if mod.use_restricted_range: start_before_cyclic, end_before_cyclic = restrict_range(start_before_cyclic, end_before_cyclic, restrict_start_point, restrict_end_point) #Make sure the start index is not 0 start_before_cyclic = max(0, start_before_cyclic) #Restore the original cyclic points before adding the difference self.bones_points[obj_bonename][start_before_cyclic : end_before_cyclic] = cyclic_points[obj_bonename][start_before_cyclic : end_before_cyclic] repeat_mp_difference(self, obj_bonename, repeat_before, difference[:-3], -length, start_p, end_p -3, mod) frame_range_reset(self, context) def get_mp_points(self, context, included = None): '''Using Blender's motion path to get the points for the editable motion path''' # print('get_mp_points') #add a timer while recalculting the motion path to avoid issues with g hotkey # self.timer_tick_counter = 0 # self.timer = context.window_manager.event_timer_add(0.1, window=context.window) active_obj = context.view_layer.objects.active #Check for the current selection and change to the relevant selection if necessery #check this as a set since the bone order can change when another object is active if context.selected_pose_bones and self.posemode: selected_items = {(bone.id_data.name, bone.name) for bone in context.selected_pose_bones} elif context.selected_objects and not self.posemode: selected_items = {(obj.id_data.name, obj.name) for obj in context.selected_objects} else: selected_items = None #if the bone selection is change then update the selection before running blender motion paths if set(self.mp_bone_names) != selected_items: if selected_items: for obj_bonename in selected_items: objname, bonename = obj_bonename obj = context.view_layer.objects[objname] context.view_layer.objects.active = obj if self.posemode: obj.data.bones[bonename].select = False else: obj.select_set(False) for obj_bonename in self.mp_bone_names: objname, bonename = obj_bonename if included: if obj_bonename not in included: continue obj = context.view_layer.objects[objname] context.view_layer.objects.active = obj if self.posemode: obj.data.bones[bonename].select = True else: obj.select_set(True) #Iterating first over the objects and then over the bones for obj_bonename in self.mp_bone_names: # Check if the bone is supposed to be included if included: if obj_bonename not in included: continue objname, bonename = obj_bonename obj = context.view_layer.objects[objname] #blender also creates motion path per active object context.view_layer.objects.active = obj range_type = 'MANUAL' if bpy.app.version >= (4, 0, 0) else 'SCENE' #Always using MANUAL because we are sampling every time separate chuncks if self.posemode: bpy.ops.pose.paths_calculate(range = range_type) else: bpy.ops.object.paths_calculate(range = range_type) #Get all the point values from all the bones in the obj from Blender's motion path for obj_bonename in self.mp_bone_names: if included: if obj_bonename not in included: continue objname, bonename = obj_bonename obj = context.view_layer.objects[objname] context.view_layer.objects.active = obj if self.posemode and bonename not in obj.pose.bones: continue item = obj.pose.bones[bonename] if self.posemode else obj if item.motion_path is None: continue length = len(item.motion_path.points)*3 points = np.zeros(length) item.motion_path.points.foreach_get('co', points) self.bones_points.update({obj_bonename : points}) #get the frames from the keyframes sorted update_mp_bones_keys(self, item, obj_bonename) # calculate all the velocities # calculate_velocities(self, context) if self.posemode: bpy.ops.pose.paths_clear(only_selected=False) else: bpy.ops.object.paths_clear(only_selected=False) # context.view_layer.objects.active = active_obj #select back the bones if set(self.mp_bone_names) != selected_items: if selected_items: for obj_bonename in selected_items: objname, bonename = obj_bonename obj = context.view_layer.objects[objname] if self.posemode: obj.data.bones[bonename].select = True else: obj.select_set(True) for obj_bonename in self.mp_bone_names: if selected_items and obj_bonename in selected_items: continue objname, bonename = obj_bonename obj = context.view_layer.objects[objname] # context.view_layer.objects.active = obj if self.posemode: obj.data.bones[bonename].select = False else: obj.select_set(False) context.view_layer.objects.active = active_obj if self.posemode: bpy.ops.pose.paths_clear(only_selected=False) else: bpy.ops.object.paths_clear(only_selected=False) #update the handles get_handles(self) def matrix_differece(self, context): '''get the difference between the world matrix and the basis matrix in local space''' self.matrix_diff = dict() if self.posemode: for bone in context.selected_pose_bones: obj = bone.id_data parent_matrix = bone.parent.matrix if bone.parent else Matrix.Identity(4) #make sure the matrix is not zero to be able to invert it matrix_basis_inverted = bone.matrix_basis.inverted() if bone.matrix_basis.determinant() else Matrix.Identity(4) matrix_diff = obj.matrix_world @ bone.matrix @ matrix_basis_inverted @ parent_matrix.inverted() self.matrix_diff.update({(bone.id_data.name, bone.name) : matrix_diff}) #to get the new world matrix use bone.parent.matrix @ matrix_diff @ bone.matrix_basis and then get the translation else: for obj in context.selected_objects: parent_matrix = obj.parent.matrix_world if obj.parent else Matrix.Identity(4) #make sure the matrix is not zero to be able to invert it matrix_basis_inverted = obj.matrix_basis.inverted() if obj.matrix_basis.determinant() else Matrix.Identity(4) matrix_diff = obj.matrix_world @ matrix_basis_inverted @ parent_matrix.inverted() self.matrix_diff.update({(obj.id_data.name, obj.name) : matrix_diff}) def handles_selection_clear(self): self.bones_selected_handles_l.clear() self.bones_selected_handles_r.clear() self.bones_handles_right.clear() self.bones_handles_left.clear() def align_handles(handle, other_handle, keyframe_coord, dist_len = None): '''Keep handles aligned - flip the handle distance from keyframe and use lerp to keep length''' other_handle_distance = ((other_handle - keyframe_coord) *-1) handle_distance = (handle - keyframe_coord) #use lerp to keep the handle length factor = handle_distance.length /other_handle_distance.length if dist_len: factor *= dist_len handle = keyframe_coord.lerp(keyframe_coord + other_handle_distance, factor) return handle def get_distance(key, self, context, bone, obj_bonename, frame, matrix_diff, coord_3d): '''get the distance between the initial coordinates and the current one and convert to matrix distance''' # parent_frame = None initial_coord = self.initial_coord if key == 'keyframe': loc_coord = Vector(self.bones_keyframes_coords[obj_bonename][frame]) initial_coord = Vector(self.initial_keyframe_coords[obj_bonename][frame]) handle_frame = None elif key == 'handle_r': if frame not in self.bones_handles_right[obj_bonename]: return loc_coord = self.bones_handles_right[obj_bonename][frame] if obj_bonename not in self.bones_selected_handles_r: return False if frame not in self.bones_selected_handles_r[obj_bonename]: return False handle_frame = self.bones_handles_frames[obj_bonename][frame][1] elif key == 'handle_l': if frame not in self.bones_handles_left[obj_bonename]: return loc_coord = self.bones_handles_left[obj_bonename][frame] if obj_bonename not in self.bones_selected_handles_l: return False if frame not in self.bones_selected_handles_l[obj_bonename]: return False handle_frame = self.bones_handles_frames[obj_bonename][frame][0] if bone.parent: if handle_frame: # handle_frame = add_frame_offset(bone.id_data, handle_frame, add = True) if handle_frame != context.scene.frame_current_final: handle_subframe, handle_frame = math.modf(handle_frame) context.scene.frame_set(int(handle_frame), subframe = handle_subframe) parent_matrix = bone.parent.matrix if self.posemode else bone.parent.matrix_world else: parent_matrix = Matrix.Identity(4) distance = (parent_matrix.inverted() @ matrix_diff.inverted() @ loc_coord) - (parent_matrix.inverted() @ matrix_diff.inverted() @ initial_coord) return distance def update_mp_handle_types(self, context, handle_value): '''update the values of the handles types from the motion path''' if not self.bones_selected_keyframes: return update_frames = [] for obj_bonename, frames in self.bones_selected_keyframes.items(): objname, bonename = obj_bonename obj = context.view_layer.objects[objname] fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action) path = get_location_path(self.posemode, obj, bonename) for i in range(3): fcu = fcurves.find(data_path = path, index = i) if fcu is None: continue for keyframe_point in fcu.keyframe_points: frame = round(keyframe_point.co[0], 2) frame = add_frame_offset(obj, frame) if frame not in frames: continue handles = get_mp_handle_side(self, obj_bonename, frame) if handle_value in {'LINEAR', 'BEZIER', 'CONSTANT'}: #in this case handle_type is actually applying interpolation keyframe_point.interpolation = handle_value else: if 'handle_r' in handles: keyframe_point.handle_right_type = handle_value if 'handle_l' in handles: keyframe_point.handle_left_type = handle_value update_frames.append(frame) fcu.update() return update_frames def get_mp_handle_side(self, obj_bonename, frame): '''In case of only one side is selected and need one handle type updated''' handles = set() if obj_bonename in self.bones_selected_handles_r: if frame in self.bones_selected_handles_r[obj_bonename]: handles.add('handle_r') if obj_bonename in self.bones_selected_handles_l: if frame in self.bones_selected_handles_l[obj_bonename]: handles.add('handle_l') if not handles: handles = {'handle_r', 'handle_l'} return handles def mp_update_keyframes(self, context, obj_bonename, frame_distance, key): '''updating the selected keyframes or handles after being moved and translated from world to local space''' # for obj_bonename in self.bones_selected_keyframes.keys(): objname, bonename = obj_bonename obj = context.view_layer.objects[objname] distance = frame_distance fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action) path = get_location_path(self.posemode, obj, bonename) for i in range(3): fcu = fcurves.find(data_path = path, index = i) if fcu is None: continue keyframes = set() for keyframe_point in fcu.keyframe_points: # frame = round(keyframe_point.co[0], 2) frame = keyframe_point.co[0] frame = round(add_frame_offset(obj, frame, add = False), 2) if frame not in self.bones_selected_keyframes[obj_bonename]: continue keyframes.add(frame) #for scale calculation get the distance per frame if self.scale or self.rotate: distance = frame_distance[frame] #update only the keyframes with handles if key == 'keyframe': keyframe_point.co[1] += distance[i] keyframe_point.handle_right[1] += distance[i] keyframe_point.handle_left[1] += distance[i] elif distance: #update only the handles #when using auto handles, update only from one side side_auto = get_side_auto(self, obj_bonename, frame, key) mp_update_keyframe_handles(self, obj_bonename, keyframe_point, frame, distance[i], 'handle_right', side_auto) mp_update_keyframe_handles(self, obj_bonename, keyframe_point, frame, distance[i], 'handle_left', side_auto) # add keyframes to channels that are missing a frame missing_frames = set(self.bones_selected_keyframes[obj_bonename]).difference(keyframes) if not missing_frames or not distance[i]: continue for missing_frame in missing_frames: missing_frame = add_frame_offset(obj, missing_frame, add = True) value = fcu.evaluate(missing_frame) if key == 'keyframe': value += distance[i] keyframe_point = fcu.keyframe_points.insert(missing_frame, value) # need to recalculate the handles and add their value # if key != 'keyframe': # get_handles(self) # distance = get_distance(key, self, context, posebone, obj_bonename, frame, self.matrix_diff[obj_bonename], None) def mp_update_keyframe_handles(self, obj_bonename, keyframe_point, frame, distance, handle, side_auto): selection = self.bones_selected_handles_r if handle == 'handle_right' else self.bones_selected_handles_l #getting the right side handles if obj_bonename not in selection: return if side_auto not in {handle[:8], None}: return if frame not in selection[obj_bonename]: return keyframe_handle = getattr(keyframe_point, handle) prev_handle = keyframe_handle.copy() obj = bpy.context.view_layer.objects[obj_bonename[0]] #getting the avg frame between all the handles avg_frame = self.bones_handles_frames[obj_bonename][frame][1] if handle == 'handle_right' else self.bones_handles_frames[obj_bonename][frame][0] avg_frame = add_frame_offset(obj, avg_frame, add = True) #reversing frame offset factor = abs((avg_frame - keyframe_point.co[0]) / (keyframe_handle[0] - keyframe_point.co[0])) #getting the value at avg handle_avg_value = keyframe_point.co.lerp(keyframe_handle, factor) handle_avg_value[1] += distance #move back to the previous handle position keyframe_handle = keyframe_point.co.lerp(handle_avg_value, 1/factor) if self.bones_handles_types[obj_bonename][frame] == 'VECTOR': keyframe_point.handle_left_type = 'FREE' keyframe_point.handle_right_type = 'FREE' self.bones_handles_types[obj_bonename][frame] = 'FREE' setattr(keyframe_point, handle, keyframe_handle) if self.bones_handles_types[obj_bonename][frame] == 'AUTO': dist_len = abs((keyframe_handle - keyframe_point.co).length / (prev_handle - keyframe_point.co).length) other_handle = 'handle_left' if handle == 'handle_right' else 'handle_right' other_keyframe_handle = getattr(keyframe_point, other_handle) other_new_value = align_handles(other_keyframe_handle, keyframe_handle, keyframe_point.co, dist_len) setattr(keyframe_point, other_handle, other_new_value) keyframe_point.handle_left_type = 'ALIGNED' keyframe_point.handle_right_type = 'ALIGNED' def get_side_auto(self, obj_bonename, frame, key): '''In case of using automatic handles, and both sides are selected and being moved''' if obj_bonename not in self.bones_handles_types: return None if frame not in self.bones_handles_types[obj_bonename]: return None if self.bones_handles_types[obj_bonename][frame] != 'AUTO': return None if obj_bonename in self.bones_selected_handles_r and obj_bonename in self.bones_selected_handles_l: if frame in self.bones_selected_handles_r[obj_bonename] and frame in self.bones_selected_handles_l[obj_bonename]: return key return None def handles_frame_remove(self, obj_bonename, frame): handles = [self.bones_selected_handles_l, self.bones_selected_handles_r, self.bones_handles_right, self.bones_handles_left] for handle in handles: if obj_bonename not in handle: continue #bones_selected_handles r using lists and bones_handles are using dict if isinstance(handle[obj_bonename], list): if frame in handle[obj_bonename]: handle[obj_bonename].remove(frame) elif isinstance(handle[obj_bonename], dict): del handle[obj_bonename][frame] if not len(handle[obj_bonename]): del handle[obj_bonename] def get_avg_handles(handle_side, keys, handles, obj): '''Get the average frame and values of all the handles''' #get the average of the frames from all the handles on all the location keyframes handles_frame_avg = sum(handle[0] for handle in handles)/len(handles) new_handles = [] for keyframe in keys: if keyframe is None: new_handles.append(None) continue handle = getattr(keyframe, handle_side) try: factor = (handles_frame_avg - keyframe.co[0]) / (handle[0] - keyframe.co[0]) except ZeroDivisionError: factor = 1 new_value = keyframe.co.lerp(handle, factor) new_handles.append(new_value[1]) # new_handles.append(handle[1]) return handles_frame_avg, new_handles def get_handle_type(self, obj_bonename, frame, keys): handles = [] for key in keys: if key is None: continue handles.append(key.handle_left_type) handles.append(key.handle_right_type) handle_type = 'FREE' if any([handle in {'FREE', 'VECTOR'}for handle in handles]) else 'AUTO' if obj_bonename in self.bones_handles_types: # print('get handle type ', frame) self.bones_handles_types[obj_bonename].update({frame: handle_type}) else: self.bones_handles_types.update({obj_bonename : {frame : handle_type}}) def find_fcu_group(obj, path): '''find the fcurve group ''' fcurves_container = Tools.get_fcurves_container(obj, obj.animation_data.action) for group in fcurves_container.groups: for channel in group.channels: if path == channel.data_path: return group return None def get_frame_keys(obj, fcurves, path, frames): frame_keys = dict() #iterate over the location arrays for i in range(3): fcu = fcurves.find(data_path = path, index = i) if fcu is None: continue for keyframe_point in fcu.keyframe_points: frame = keyframe_point.co[0] frame = round(add_frame_offset(obj, frame), 2) if frame not in frames: continue if keyframe_point.interpolation != 'BEZIER': continue if frame not in frame_keys: #create an empty handle, in case one of the handles doesn't exist frame_keys.update({frame : [None, None, None]}) frame_keys[frame][i] = keyframe_point return frame_keys def add_layer_to_handles(obj, path, frame, handle_right_values, handle_left_values): if not obj.animation_data.use_nla: return handle_right_values, handle_left_values if not obj.animation_data.use_tweak_mode and obj.animation_data.action: if obj.animation_data.action_influence == 1 and obj.animation_data.action_blend_type == 'REPLACE': return handle_right_values, handle_left_values blend_types = {'REPLACE' : '+', 'ADD' : '+', 'COMBINE' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'} value_add = Vector((0, 0, 0)) handle_right_diff = Vector((0, 0, 0)) handle_left_diff = Vector((0, 0, 0)) for track in obj.animation_data.nla_tracks: if track.mute: continue for strip in track.strips: if strip.mute: continue fcurves = Tools.get_fcurves_channelbag(obj, strip.action) for i in range(3): fcu = fcurves.find(data_path = path, index = i) if fcu is None: continue #get the difference value between the keyframe and the handle, and add the overall values to it if strip.action == obj.animation_data.action: handle_right_diff[i] = handle_right_values[i] - fcu.evaluate(frame) handle_left_diff[i] = handle_left_values[i] - fcu.evaluate(frame) value_add[i] = handles_add_to_value(value_add[i], fcu, frame, strip.influence, strip.blend_type, blend_types) #In case not in tweak mode and there is an action on top of all the tracks if not obj.animation_data.use_tweak_mode and obj.animation_data.action: fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action) for i in range(3): fcu = fcurves.find(data_path = path, index = i) if fcu is None: continue #get the difference value between the keyframe and the handle, and add the overall values to it handle_right_diff[i] = handle_right_values[i] - fcu.evaluate(frame) handle_left_diff[i] = handle_left_values[i] - fcu.evaluate(frame) value_add[i] = handles_add_to_value(value_add[i], fcu, frame, obj.animation_data.action_influence, obj.animation_data.action_blend_type, blend_types) handle_right_values = handle_right_diff + value_add handle_left_values = handle_left_diff + value_add return handle_right_values, handle_left_values def handles_add_to_value(value_add, fcu, frame, influence, blend_type, blend_types): if blend_type =='REPLACE': value_add = value_add * (1 - influence) + fcu.evaluate(frame) * influence else: value_add = eval('value_add' + blend_types[blend_type] +' fcu.evaluate(frame)' + '*' + str(influence)) return value_add def get_handles(self): '''Getting the coordinates and frames of the handles''' if not self.bones_selected_keyframes: return if not bpy.context.scene.emp.handles: return scene = bpy.context.scene frame_current = scene.frame_current_final for obj_bonename, frames in self.bones_selected_keyframes.items(): obj = bpy.context.view_layer.objects[obj_bonename[0]] bone = obj.pose.bones[obj_bonename[1]] if self.posemode else obj path = get_location_path(self.posemode, obj, obj_bonename[1]) fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action) #get all the keyframes and handles in a dictionary with rounded frames as the keys frame_keys = get_frame_keys(obj, fcurves, path, frames) #Get the average frame and then the value of the handles in this avg point for frame, keys in frame_keys.items(): # frame = add_frame_offset(obj, frame) handles_right = [keyframe.handle_right for keyframe in keys if keyframe is not None] handles_left = [keyframe.handle_left for keyframe in keys if keyframe is not None] handles_right_frame, handle_right_values = get_avg_handles('handle_right', keys, handles_right, obj) handles_left_frame, handle_left_values = get_avg_handles('handle_left', keys, handles_left, obj) handles_right_frame = add_frame_offset(obj, handles_right_frame, add = False) handles_left_frame = add_frame_offset(obj, handles_left_frame, add = False) get_handle_type(self, obj_bonename, frame, keys) # to get the new world matrix use bone.parent.matrix @ matrix_diff @ bone.matrix_basis and then get the translation scene.frame_set(int(handles_right_frame), subframe = handles_right_frame % 1) bone_right_matrix = bone.matrix.copy() if self.posemode else bone.matrix_world.copy() bone_right_matrix_basis = bone.matrix_basis.copy() #if a keyframe is missing then get the array from the matrix basis if None in keys: handle_right_values = [bone.matrix_basis.translation[i] if key is None else handle_right_values[i] for i, key in enumerate(keys)] scene.frame_set(int(handles_left_frame), subframe = handles_left_frame % 1) bone_left_matrix = bone.matrix.copy() if self.posemode else bone.matrix_world.copy() bone_left_matrix_basis = bone.matrix_basis.copy() if None in keys: handle_left_values = [bone.matrix_basis.translation[i] if key is None else handle_left_values[i] for i, key in enumerate(keys)] handle_right_values, handle_left_values = add_layer_to_handles(obj, path, frame, handle_right_values, handle_left_values) #converting the handle values to 3d coordinates handle_right_coords = bone_right_matrix @ bone_right_matrix_basis.inverted() @ Vector(handle_right_values) handle_left_coords = bone_left_matrix @ bone_left_matrix_basis.inverted() @ Vector(handle_left_values) if self.posemode: handle_right_coords = obj.matrix_world @ handle_right_coords handle_left_coords = obj.matrix_world @ handle_left_coords #update the coordinates of the handles if obj_bonename in self.bones_handles_left: self.bones_handles_right[obj_bonename].update({frame : handle_right_coords}) self.bones_handles_left[obj_bonename].update({frame : handle_left_coords}) self.bones_handles_frames[obj_bonename].update({frame : (handles_left_frame, handles_right_frame)}) #get the relation between the keyframe frames and the handle frames else: self.bones_handles_right.update({obj_bonename : {frame : handle_right_coords}}) self.bones_handles_left.update({obj_bonename : {frame : handle_left_coords}}) self.bones_handles_frames.update({obj_bonename : {frame : (handles_left_frame, handles_right_frame)}}) if scene.frame_current_final != frame_current: scene.frame_set(int(frame_current), subframe = frame_current%1) coords_2d_update(self, bpy.context) return def mp_handles_on_off(self, context): '''Turn on or off motion path handles''' if self.mp_handles == context.scene.emp.handles: return if context.scene.emp.handles: get_handles(self) else: #reset all the handle values when turning off self.bones_handles_right = dict() self.bones_handles_left = dict() self.bones_selected_handles_r = dict() self.bones_selected_handles_l = dict() self.bones_handles_frames = dict() self.coords2d_handles_r = dict() self.coords2d_handles_l = dict() self.update = True Tools.redraw_areas(['VIEW_3D']) self.mp_handles = context.scene.emp.handles def compare_bone_keys(self,context, event): '''check if the keyframe values were changed''' global anim_update if not anim_update: return False current_bones_keys = get_current_bones_keys(self) if self.current_bones_keys == current_bones_keys: return False if not current_bones_keys: return False if not self.current_bones_keys: return False frames = [] skip = True for obj_bonename, values in self.current_bones_keys.items(): if obj_bonename not in current_bones_keys: # object might have not been selected, then just continue and eventually skip continue if self.current_bones_keys[obj_bonename] == current_bones_keys[obj_bonename]: continue skip = False if not len(current_bones_keys[obj_bonename]) and len(self.current_bones_keys[obj_bonename]): continue if len(current_bones_keys[obj_bonename]) and not len(self.current_bones_keys[obj_bonename]): continue #creating a new 2D array, with frames and then values old_array = np.stack((values[::2], values[1::2])) new_array = np.stack((current_bones_keys[obj_bonename][::2], current_bones_keys[obj_bonename][1::2])) # Detect first and last frames if they moved on the timeline update the whole path old_first_frame, old_last_frame = round(old_array[0][0]), round(old_array[0][-1]) new_first_frame, new_last_frame = round(new_array[0][0]), round(new_array[0][-1]) if round(old_first_frame) != round(new_first_frame) or round(old_last_frame) != round(new_last_frame): frames = [] global frame_range_update frame_range_update = True # mp_frame_range_change(self, context) continue if old_array.shape[1] != new_array.shape[1]: #checking if frames were added or removed old_array_indices = np.where(np.isin(old_array[0], new_array[0], invert=True))[0] new_array_indices = np.where(np.isin(new_array[0], old_array[0], invert=True))[0] #checking if frames were added or removed old_array_indices = np.where(np.isin(old_array[0], new_array[0], invert=True))[0] new_array_indices = np.where(np.isin(new_array[0], old_array[0], invert=True))[0] #Getting all the new frames frames = [old_array[0][i] for i in old_array_indices] frames += [new_array[0][i] for i in new_array_indices] else: #Checking if any values or frames were changed #finding the difference between the arrays diff_values = np.round(old_array[1], decimals = 3) - np.round(new_array[1], decimals = 3) diff_frames = np.round(old_array[0], decimals = 2) - np.round(new_array[0], decimals = 2) #checking if there are values or frames that are not 0 after substraction i_values = np.nonzero(diff_values) i_frames = np.nonzero(diff_frames) #flatten the array frames = np.union1d(new_array[0][i_frames[0]], new_array[0][i_values[0]]) obj = context.view_layer.objects[obj_bonename[0]] frames = list(map(lambda frame: add_frame_offset(obj, frame), frames)) if skip: # print('skipping') return False self.initial_keyframe_coords = copy.deepcopy(self.bones_keyframes_coords) if len(frames): update_mp_points(self, context, frames, None, None) else: # print('get_mp_points') get_mp_points(self, context) calculate_velocities(self, context) update_keyframe_coords(self) coords_2d_update(self, context) self.current_bones_keys = current_bones_keys # Update the drawing self.update = True #update selected keyframes in case they were removed for obj_bonename, selected_frames in self.bones_selected_keyframes.items(): key_frames = set(self.mp_bones_keys[obj_bonename]) if not set(selected_frames).issubset(key_frames): for frame in selected_frames: if frame not in key_frames: self.bones_selected_keyframes[obj_bonename].remove(frame) handles_frame_remove(self, obj_bonename, frame) #store selected keyframes in a property because of undo/redo context.scene.emp.selected_keyframes = serialize_dict(self.bones_selected_keyframes) return True def compare_obj_layers(self, context, event): '''redraw motion path if layer properties changed''' def refresh_motion_path(old_bones_keys): get_mp_points(self, context) update_frame_offsets(self, context, old_bones_keys) update_keyframe_coords(self) get_handles(self) calculate_velocities(self, context) # Get the coords of the keyframes coords_2d_update(self, context) self.update = True global layers_update if not layers_update: return layers_update = False obj = context.object if not obj.animation_data: return if not obj.animation_data.use_nla: if len(self.obj_layer_properties): refresh_motion_path(self.mp_bones_keys) self.obj_layer_properties = [] return if not len(obj.animation_data.nla_tracks): return if event.value not in {'NOTHING', 'RELEASE'} and event.type not in {'INBETWEEN_MOUSEMOVE', 'MOUSEMOVE', self.select_mouse}: return if self.obj_layer_properties: #Action is the first item, checking if it was changed on layer change if context.object.animation_data.action != self.obj_layer_properties[0]: self.bones_selected_keyframes = dict() self.bones_selected_handles_r = dict() self.bones_selected_handles_l = dict() # anim_update = True obj_layer_properties = get_layer_properties(self.items) if self.obj_layer_properties == obj_layer_properties: return old_bones_keys = self.mp_bones_keys.copy() refresh_motion_path(old_bones_keys) self.obj_layer_properties = obj_layer_properties def update_frame_offsets(self, context, old_bones_keys): '''update all the frame offsets for selected keyframes''' for obj_bonename, frames in self.bones_selected_keyframes.items(): obj = context.view_layer.objects[obj_bonename[0]] #create a dictionary with the old as keys and new frames as values old_new_frames = dict(zip(old_bones_keys[obj_bonename], self.mp_bones_keys[obj_bonename])) self.bones_selected_keyframes[obj_bonename] = [old_new_frames.get(frame) for frame in frames] keyframe_coords = self.bones_keyframes_coords[obj_bonename] self.bones_keyframes_coords[obj_bonename] = {old_new_frames.get(frame) : coords for frame, coords in keyframe_coords.items()} if obj_bonename in self.bones_selected_handles_l: handles_l = self.bones_selected_handles_l[obj_bonename] self.bones_selected_handles_l[obj_bonename] = [old_new_frames.get(frame) for frame in handles_l] if obj_bonename in self.bones_selected_handles_r: handles_r = self.bones_selected_handles_r[obj_bonename] self.bones_selected_handles_r[obj_bonename] = [old_new_frames.get(frame) for frame in handles_r] def get_layer_properties(items): '''get all the layer properties to check if something has changed''' for item in items: obj = item.id_data layer_properties = [] if not obj.animation_data.use_nla: return layer_properties if not obj.animation_data.use_tweak_mode and obj.animation_data.action: if obj.animation_data.action_influence == 1 and obj.animation_data.action_blend_type == 'REPLACE': return layer_properties if not len(obj.animation_data.nla_tracks): return layer_properties layer_properties.append(obj.animation_data.action) track_props = {'is_solo', 'mute'} for track in obj.animation_data.nla_tracks: layer_properties.append(track) for prop in track_props: layer_properties.append(prop) if track.mute: continue for strip in track.strips: layer_properties.append(strip) for prop in dir(strip): value = getattr(strip, prop) if not isinstance(value, int) and not isinstance(value, float) and not isinstance(value, bool): continue if strip.is_property_readonly(prop): continue layer_properties.append(value) #get custom frame range property from anim layers addon if hasattr(obj, 'Anim_Layers') and hasattr(obj, 'als') and len(obj.Anim_Layers): layer = obj.Anim_Layers[obj.als.layer_index] if hasattr(layer, 'frame_range'): layer_properties.append(layer.frame_range) elif hasattr(layer, 'custom_frame_range'): layer_properties.append(layer.custom_frame_range) return layer_properties def update_keyframe_coords(self): self.bones_keyframes_coords = dict() # test = dict() # for obj_bonename, points in self.bones_points.items(): # keyframes = self.mp_bones_keys[obj_bonename] # keyframes_coords = dict() # vectors = points.reshape(-1, 3) # # Generating frame numbers # frame_indices = np.arange(len(vectors), dtype=float) # # Adding the start offset # frames = frame_indices + self.avz_frame_start[obj_bonename] # # Apply frame offset to all frames at once # frames_with_offset = np.array([ # add_frame_offset(bpy.data.objects[obj_bonename[0]], frame, add=True) # for frame in frames # ]) # mask = np.isin(frames_with_offset, keyframes) # # Get valid frames and corresponding coordinates # valid_frames = frames_with_offset[mask] # valid_coords = vectors[mask] # # Create the dictionary # # print(f'valid frames {valid_frames} coords {valid_coords}') # keyframes_coords = dict(zip(valid_frames.tolist(), valid_coords)) # # self.bones_keyframes_coords.update({obj_bonename : keyframes_coords}) # test[obj_bonename] = keyframes_coords for obj_bonename, points in self.bones_points.items(): keyframes = self.mp_bones_keys[obj_bonename] # print('keyframes ', keyframes) keyframes_coords = dict() for i in range(0, len(points), 3): coords = points[i : i+3] frame = ((i+3) / 3) + self.avz_frame_start[obj_bonename] - 1 # frame = add_frame_offset(bpy.data.objects[obj_bonename[0]], frame, add = True) if frame not in keyframes: continue # print('coords ', coords, type(coords)) keyframes_coords.update({frame : coords}) # print(f'valid frames {frame} coords {coords}') self.bones_keyframes_coords.update({obj_bonename : keyframes_coords}) # print('3267 keyframe coords ', self.bones_keyframes_coords) # print(f'test ', test,'\n') # print(f'keyframes coords {self.bones_keyframes_coords}\n') def get_mp_items(self): '''Get the objects or bones that are included in the motion path since it's getting broken when using undo''' if self.posemode: self.items = [bpy.data.objects[objname].pose.bones[bonename] for objname, bonename in self.mp_bone_names] else: self.items = [bpy.data.objects[objname] for objname, bonename in self.mp_bone_names] def get_current_bones_keys(self): '''create the dictionary from the current selected bones keyframes values to compare and check if they changed. the keyframes include all channels and bezier handle values''' #if we are currently moving the selection with motion path, then we just need to check the location keyframes current_selected_items = bpy.context.selected_pose_bones if self.posemode else bpy.context.selected_objects if not current_selected_items: return None current_bones_keys = dict() transforms = ['location', 'rotation', 'scale'] # Checking if it's the motion path objects/bones selected then get just location # if it's a different controller then check all transform channels. if all(selection in self.items for selection in current_selected_items): transforms = ['location'] items = self.items else: items = current_selected_items key_props = ['co', 'handle_left','handle_right']# 'handle_left_type', 'handle_right_type' , 'interpolation' for item in items: # Replace the rotation path with the actual rotation of the selection if 'rotation' in transforms: transforms[1] = 'rotation_euler' if len(item.rotation_mode) == 3 else 'rotation_' + item.rotation_mode.lower() #create the dictionary of the bones and their keyframes obj_bonename = (item.id_data.name, item.name) current_bones_keys.update({obj_bonename : []}) #add all the keyframes and interpolations for transform in transforms: for prop in key_props: current_bones_keys[obj_bonename] = list(get_bone_keyframes(item, transform, prop, current_bones_keys[obj_bonename])) return current_bones_keys def get_bone_keyframes(bone, transform = 'location', property = 'co', bones_keys = []): '''get the keyframes of the selected bones or objects''' def get_property(length): # For single keyframes skip the handles if 'handle' in property and length == 2: return [] if property != 'interpolation': keyframes = np.zeros(length) # keyframes = [None]*length fcu.keyframe_points.foreach_get(property, keyframes) # Removing the first handle from the left if property == 'handle_left': keyframes = keyframes[2:] # Removing the last handle from the right if property == 'handle_right': keyframes = keyframes[:-2] else: keyframes = [] for keyframe in fcu.keyframe_points: keyframes.append(keyframe.interpolation) keyframes.append(keyframe.handle_left_type) keyframes.append(keyframe.handle_right_type) return keyframes if transform == 'rotation_quaternion' or transform == 'rotation_axis_angle': array_len = 4 else: array_len = 3 #get the keyframes bone_path = bone.path_from_id() + '.' if type(bone) == bpy.types.PoseBone else '' obj = bone.id_data if obj.animation_data is None: return [] if obj.animation_data.action is None: return [] fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action) for i in range(array_len): fcu = fcurves.find(data_path = bone_path + transform, index = i) if fcu is None: continue if fcu.mute: continue length = len(fcu.keyframe_points) * 2 keyframes = get_property(length) bones_keys = np.append(bones_keys, keyframes) return bones_keys def clear_names_owner(): global names_owner if 'names_owner' in globals(): bpy.msgbus.clear_by_owner(names_owner) del names_owner def subscribe_to_names(emp): '''subscribe_to_object and bone names in case they were changed by the user''' global names_owner names_owner = object() subscribe_object = (bpy.types.Object, 'name') subscribe_posebone = (bpy.types.PoseBone, 'name') subscribe_bone = (bpy.types.Bone, 'name') for subscribe in [subscribe_object, subscribe_posebone, subscribe_bone]: bpy.msgbus.subscribe_rna( key=subscribe, owner=names_owner, args=(), notify=update_names,) def update_names(): global names_update names_update = True def check_bone_names(self, context): #check if the object or bone name was changed global names_update if not names_update: return names_update = False changed = False for i, obj_bonename in enumerate(self.mp_bone_names): obj_name, bonename = obj_bonename #find the object name if it was changed if obj_name not in context.view_layer.objects: for obj in context.view_layer.objects: if 'atb_mp_name' not in obj.keys(): continue if obj['atb_mp_name'] == obj_name: self.mp_bone_names[i] = (obj.name, bonename) update_dictionaries_keys(self, (obj.name, bonename), obj_bonename) obj['atb_mp_name'] = obj.name break changed = True else: obj = context.view_layer.objects[obj_name] if obj is None: continue if not self.posemode: continue #check if bone names were changed if bonename not in obj.pose.bones: for bone in obj.pose.bones: if 'atb_mp_name' not in bone.keys(): continue if bone['atb_mp_name'] == bonename: #if it found the old name in the property then update all the attributes with the new name self.mp_bone_names[i] = (obj.name, bone.name) update_dictionaries_keys(self, (obj.name, bone.name), obj_bonename) bone['atb_mp_name'] = bone.name break changed = True return changed def update_dictionaries_keys(self, new_names, old_names): '''update all the attributes with the new object and bone names, replacing the dict keys but keeping the values using this when name of objects or bones was changed''' dicts_to_update = [self.bones_points, self.mp_bones_keys, self.frame_start, self.frame_end, self.avz_frame_start, self.bones_keyframes_coords, self.matrix_diff, self.bones_selected_keyframes,self.bones_selected_handles_r, self.bones_selected_handles_l, self.bones_handles_right, self.bones_handles_left, self.bones_handles_frames, self.bones_handles_types, self.mp_bones_keys, self.current_bones_keys] # Adding the bone names from the 2d dictionaries where bone_frame is inside the values dicts_to_update += list(self.coords2d_bones_frames.values()) dicts_to_update += list(self.coords2d_handles_r.values()) dicts_to_update += list(self.coords2d_handles_l.values()) # Perform the update for each dictionary for d in dicts_to_update: if old_names not in d: continue d[new_names] = d.pop(old_names) # coords_2d_update(self, bpy.context) ######################################################################################################################## class GoToKeyframe(bpy.types.Operator): """Go to the last selected Frame""" bl_idname = "anim.go_to_keyframe" bl_label = "Go To Active Keyframe" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): return context.scene.emp.motion_path def execute(self, context): selected_keyframes = context.scene.emp.selected_keyframes if not len(selected_keyframes): return {'CANCELLED'} # Converting str to dict bones_selected_keyframes = deserialize_dict(selected_keyframes) if not len(bones_selected_keyframes): return {'CANCELLED'} # Flattening the list from multiple selections values = [int(value) for selection in bones_selected_keyframes.values() for value in selection] active_frame = values[-1] context.scene.frame_set(active_frame) return {'FINISHED'} def update_prop_callback(self, context): if self.vis_type == 'VELOCITY': if self.clamp_min > self.clamp_max: self.clamp_min = self.clamp_max global velocity_update velocity_update = True self.update = True context.area.tag_redraw() def check_velocity_update(self, context): global velocity_update if 'velocity_update' not in globals(): return False if velocity_update: calculate_velocities(self, context) velocity_update = False self.update = True return True return False def default_colors(self, context): if self.vis_type != 'VELOCITY': if (self.color_after.r, self.color_after.g, self.color_after.b) == (0.0, 0.0, 1.0): self['color_after'] = (0.0, 1.0, 0.0) self.update = True else: global velocity_update velocity_update = True if (self.color_after.r, self.color_after.g, self.color_after.b) == (0.0, 1.0, 0.0): self['color_after'] = (0.0, 0.0, 1.0) class EditableMotionPathSettings(bpy.types.PropertyGroup): '''All the settings for Editable motion path''' motion_path: bpy.props.BoolProperty(name = "Motion Path", description = "Flag when Motion Path is on", default = False, override = {'LIBRARY_OVERRIDABLE'}) settings: bpy.props.BoolProperty(name = "Motion Path Settings", description = "Open the settings Menu", default = False, override = {'LIBRARY_OVERRIDABLE'}) # mp_keyframe_scale: bpy.props.FloatProperty(name = "Scale Selecgted Keyframes Bounding Box", description = "Change the scale of the bounding box around the selected keyframes ", default = 0.1, step = 0.1, precision = 3) color_before: bpy.props.FloatVectorProperty(name="Motion Path Before Color", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, description="Motion path color before the current frame", update=update_prop_callback) color_after: bpy.props.FloatVectorProperty(name="Motion Path After Color", subtype='COLOR', default=(0.0, 1.0, 0.0), min=0.0, max=1.0, description="Motion path color before the current frame", update=update_prop_callback) infront: bpy.props.BoolProperty(name = "Motion Path In Front", description = "Display motion path in front of all the objects", default = True, override = {'LIBRARY_OVERRIDABLE'}) points: bpy.props.BoolProperty(name = "Motion Path Points", description = "Display motion path points", default = True, override = {'LIBRARY_OVERRIDABLE'}) lines: bpy.props.BoolProperty(name = "Motion Path Lines", description = "Display motion path lines", default = True, override = {'LIBRARY_OVERRIDABLE'}) handles: bpy.props.BoolProperty(name = "Motion Path Handles", description = "Display motion path handles on keyframe selection", default = True, override = {'LIBRARY_OVERRIDABLE'}) display_frames: bpy.props.BoolProperty(name = "Frame Numbers", description = "Display frame numbers on all the keyframes", default = False, override = {'LIBRARY_OVERRIDABLE'}) handle_types: bpy.props.EnumProperty(name = 'Set Keyframe Handle Type', description="Set handle type for selected keyframes", default = 'AUTO', update = update_handle_type_prop, items = [('FREE', 'Free', 'Free', 'HANDLE_FREE', 0), ('ALIGNED','Aligned', 'Aligned', 'HANDLE_ALIGNED', 1), ('VECTOR', 'Vector', 'Vector', 'HANDLE_VECTOR', 2), ('AUTO','Automatic', 'Automatic', 'HANDLE_AUTO', 3), ('AUTO_CLAMPED','Auto Clamped', 'Auto Clamped', 'HANDLE_AUTOCLAMPED', 4)]) interpolation: bpy.props.EnumProperty(name = 'Set Interpolation', description="Set Keyframe Interpolation", default = 'BEZIER', update = update_interpolation_prop, items = [('BEZIER', 'Bezier', 'Bezier', 'IPO_BEZIER', 0), ('LINEAR','Linear', 'Linear', 'IPO_LINEAR', 1), ('CONSTANT', 'Constant', 'Constant', 'IPO_CONSTANT', 2)]) frame_range: bpy.props.EnumProperty(name = 'Frame Range', description="Type of Frame Range", default = 'SCENE', update = update_frame_range_type, items = [('KEYS_ALL', 'All_Keys','Use the Scene Frame Length for the Range', 0), ('SCENE', 'Scene','Use the Scene Frame Length for the Range', 1), ('MANUAL', 'Manual','Custom Frame range using numerical input or the markers frame ranger', 2), ('AROUND', 'Around Frames','Show only around the current frame', 3)]) frame_start: bpy.props.IntProperty(name = "Frame Start", description = "Define the motion path frame start", min = 0, update = update_manual_frame_range) frame_end: bpy.props.IntProperty(name = "Frame End", description = "Define the motion path frame start", min = 0, update = update_manual_frame_range) before: bpy.props.IntProperty(name = "Before", description = "Show the frames Before the current frame", min = 0, default = 10, update=update_prop_callback) after: bpy.props.IntProperty(name = "After", description = "Show the frames After the current frame", min = 0, default = 10, update=update_prop_callback) # Velocity properties clamp_min: bpy.props.IntProperty(name = "Clamp Min", description = "Sets the clampping for the minimum values of velocity", min = 0, max = 100, default = 5, update=update_prop_callback) clamp_max: bpy.props.IntProperty(name = "Clamp Max", description = "Sets the clampping for the minimum values of velocity", min = 0, max = 100, default = 95, update=update_prop_callback) velocity_factor: bpy.props.FloatProperty(name = "Blending Factor", description = "Use a factor value to set the blending and contrast between the colors", min = 0.1, max = 5, default = 1, update=update_prop_callback) display_size: bpy.props.BoolProperty(name = "Size and Thickness", description = "Set the Line Thickness and point size", default = False, override = {'LIBRARY_OVERRIDABLE'}) thickness: bpy.props.FloatProperty(name = "Motion Path Thickness", description = "The Width of the motion path", min = 0.1, default = 1) keyframe_size: bpy.props.FloatProperty(name = "Keyframe Size", description = "The Size of the keyframe size", min = 1, default = 8) frame_size: bpy.props.FloatProperty(name = "Frame Size", description = "The Size of the frame points", min = 1, default = 6) selection_size: bpy.props.FloatProperty(name = "Selection Size", description = "The Size of the points around the selection", min = 0, default = 10) vis_type: bpy.props.EnumProperty(name = 'Visualisation type', description="Set handle type for selected keyframes", default = 'BEFORE_AFTER', update = default_colors, items = [('BEFORE_AFTER', 'Before-After', 'Before After', 0), ('ALTERNATE', 'Alternate', 'Alternating between the colors on each frame', 1), ('VELOCITY','Veclocity', 'Veclocity colors based on spacing', 2)]) update: bpy.props.BoolProperty(name = "Update", default = False) # Used for undo purpose using json selected_keyframes: bpy.props.StringProperty(name="Selected Keyframes", description="Serialized representation of selected keyframes", default = "{}") ######################################################################################################################## classes = (MotionPathOperator, EditableMotionPathSettings, GoToKeyframe) def register(): from bpy.utils import register_class for cls in classes: register_class(cls) bpy.types.Scene.emp = bpy.props.PointerProperty(type = EditableMotionPathSettings, override = {'LIBRARY_OVERRIDABLE'}) def unregister(): from bpy.utils import unregister_class for cls in classes: unregister_class(cls) del bpy.types.Scene.emp