3670 lines
167 KiB
Python
3670 lines
167 KiB
Python
# ***** 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
|