4969 lines
214 KiB
Python
4969 lines
214 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(self.region, self.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 bone_frame is None:
|
|
return
|
|
|
|
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):
|
|
'''Applying the G R S hotkeys'''
|
|
# Reseting before movement, need to check if this is really necessery
|
|
|
|
# esc = True if not any([self.press, self.rotate, self.scale]) and hasattr(self, 'draw_handle') else False
|
|
# 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)
|
|
|
|
# Need to check if this is necessery since its already happenning on mouse presse
|
|
|
|
initialize_points(self)
|
|
initialize_prop_edit_coord2d(self, bpy.context, obj_bonename)
|
|
|
|
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)
|
|
|
|
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(self, context, initial_coord, new_coord, pivot_2d):
|
|
'''rotation the points in 2d'''
|
|
new_coord_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(self.region, self.region_data, new_coord)
|
|
initial_coord_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(self.region, self.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_transform_channel(posemode, context, posebone):
|
|
'''Get the transform channel for the motion path'''
|
|
if posemode:
|
|
# Getting path depending on the channel settings in the motion path
|
|
if context.scene.emp.channels == 'HEADS':
|
|
transform = 'location'
|
|
else:
|
|
# Get the rotation channel depending the rotation mode
|
|
rot_mod = posebone.rotation_mode
|
|
transform = Tools.rot_mode_to_channel(rot_mod)
|
|
else:
|
|
transform = 'location'
|
|
|
|
return transform
|
|
|
|
def get_fcu_path(context, posemode, obj, bonename):
|
|
'''get the datapath of the location, check if it's a bone or object'''
|
|
if posemode:
|
|
posebone = obj.pose.bones[bonename]
|
|
bone_path = posebone.path_from_id()
|
|
# Getting path depending on the channel settings in the motion path
|
|
transform = get_transform_channel(posemode, context, posebone)
|
|
|
|
path = bone_path + '.' + transform
|
|
else:
|
|
path = 'location'
|
|
|
|
array_len = 4 if path.endswith('rotation_quaternion') or path.endswith('rotation_axis_angle') else 3
|
|
|
|
return path, array_len
|
|
|
|
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):
|
|
'''Zoom into selected keyframes'''
|
|
if event.type != 'NUMPAD_PERIOD':
|
|
return False
|
|
if not self.bones_selected_keyframes:
|
|
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)
|
|
self.region_data.view_location = avg_loc
|
|
max_loc = max([Vector(loc) for loc in locations])
|
|
min_loc = min([Vector(loc) for loc in locations])
|
|
self.region_data.view_distance = (max_loc - min_loc).length
|
|
if self.region_data.view_distance < 1:
|
|
self.region_data.view_distance = 1
|
|
self.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)
|
|
|
|
# Initial coords when the mouse was clicking
|
|
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, context)
|
|
coords_2d_update(self, context)
|
|
initialize_g_r_s(self, 'G')
|
|
|
|
initialize_points(self)
|
|
initialize_prop_edit_coord2d(self, context, obj_bonename)
|
|
|
|
self.prev_event_value = 'PRESS'
|
|
# self.initial_coord = Vector(coord_3d)
|
|
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 initialize_prop_edit_coord2d(self, context, obj_bonename):
|
|
# Get the position for the circle during proportional editing
|
|
# Flipping the 2d coords dictionary to get the coords based on frames for the edit prop frames
|
|
if not context.scene.emp.prop_edit:
|
|
return
|
|
self.bones_frames_coords2d = dict()
|
|
for coords2d, bones_frames in self.coords2d_bones_frames.items():
|
|
for bone, frame in bones_frames.items():
|
|
if bone not in self.bones_frames_coords2d:
|
|
self.bones_frames_coords2d[bone] = dict()
|
|
self.bones_frames_coords2d[bone][frame] = coords2d
|
|
|
|
# Getting the coord2d where the circle is going to be displayed
|
|
frame = self.current_hover_frame[1]
|
|
coords2d = self.bones_frames_coords2d[obj_bonename][frame]
|
|
self.prop_edit_mouse = coords2d
|
|
|
|
def initialize_points(self):
|
|
"""Storing all the motion path points before moving them"""
|
|
# if key == 'keyframe':
|
|
self.initial_keyframe_coords = copy.deepcopy(self.bones_keyframes_coords)
|
|
self.initial_bones_points = copy.deepcopy(self.bones_points)
|
|
|
|
self.initial_handles_right = copy.deepcopy(self.bones_handles_right)
|
|
self.initial_handles_left = copy.deepcopy(self.bones_handles_left)
|
|
|
|
def clear_reselection(self, obj_bonename, key, frame, selection):
|
|
if key == 'keyframe':
|
|
selection.clear()
|
|
handles_selection_clear(self)
|
|
selection[obj_bonename] = [frame]
|
|
|
|
def coords_cursor_offset(item, coord):
|
|
if 'mp_cursor_offset' not in item.keys():
|
|
return
|
|
coord += Vector(item['mp_cursor_offset'])
|
|
|
|
def add_cursor_offset(self, context, obj_bonename, item):
|
|
'''Currently still not applied since the motion path is having only the information of location
|
|
Can maybe add it after including also rotations'''
|
|
if not context.scene.emp.cursor_offset:
|
|
if 'mp_cursor_offset' in item.keys():
|
|
del item['mp_cursor_offset']
|
|
return
|
|
|
|
if 'mp_cursor_offset' not in item.keys():
|
|
c_loc = context.scene.cursor.matrix.translation
|
|
if type(item) == bpy.types.PoseBone:
|
|
if context.scene.emp.channels == 'HEADS':
|
|
item_loc = item.head
|
|
else:
|
|
item_loc = item.tail
|
|
else:
|
|
item_loc = item.matrix_world.translation
|
|
|
|
item['mp_cursor_offset'] = c_loc - item_loc
|
|
|
|
if self.camera_space:
|
|
cam = context.scene.camera
|
|
M_cam = np.array(cam.matrix_world, dtype=np.float32)
|
|
# Getting the camera orientation from its matrix
|
|
R = np.array(M_cam[:3, :3], dtype=np.float32)
|
|
|
|
if self.cursor_offset and 'mp_cursor_offset' in item:
|
|
# Add the camera space to the cursor offset
|
|
cursor_offset_cam = item['mp_cursor_offset'] @ R
|
|
# Adding the cursor offset to the camera space points
|
|
self.camspace_points[obj_bonename] += cursor_offset_cam
|
|
|
|
distance = item['mp_cursor_offset']
|
|
|
|
self.bones_points[obj_bonename] = self.bones_points[obj_bonename] + distance
|
|
|
|
|
|
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 != self.region_data.view_matrix or self.prev_window_matrix != self.region_data.window_matrix:
|
|
self.prev_view_matrix = self.region_data.view_matrix.copy()
|
|
self.prev_window_matrix = self.region_data.window_matrix.copy()
|
|
coords_2d_update(self, context)
|
|
|
|
mouse_hover_keyframe(self, event)
|
|
|
|
if scene.emp.smooth:
|
|
return False
|
|
|
|
# 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, array_len = get_fcu_path(context, self.posemode, obj, obj_bonename[1])
|
|
fcurves = Tools.get_fcurves(obj, obj.animation_data.action)
|
|
|
|
#if there is a keyframe then remove it
|
|
if frame in key_frames:
|
|
|
|
#removing keyframes
|
|
for i in range(array_len):
|
|
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)
|
|
|
|
#reset the motion path
|
|
initialize_points(self)
|
|
update_mp_points(self, context, [frame], [obj_bonename])
|
|
|
|
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()
|
|
|
|
get_handles(self, context)
|
|
|
|
update_keyframe_coords(self)
|
|
calculate_velocities(self, context)
|
|
|
|
coords_2d_update(self, context)
|
|
self.current_bones_keys = get_current_bones_keys(self, context)
|
|
|
|
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 left_mouse_press(self, context):
|
|
return True
|
|
|
|
if event.value == 'RELEASE' and (self.press or self.scale or self.rotate):
|
|
obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self)
|
|
# When releasing directly after press it will cancel the movement and reselect
|
|
if self.prev_event_value == 'PRESS':
|
|
if not self.shift:
|
|
# Reselect a single frame if shift is not on
|
|
clear_reselection(self, obj_bonename, key, frame, selection)
|
|
get_handles(self, context)
|
|
coords_2d_update(self, context)
|
|
cancel_update(self)
|
|
self.prev_event_value = event.value
|
|
return True
|
|
|
|
apply_keyframe_movement(self, context, event, key, coord_3d)
|
|
remove_notification(self, context)
|
|
|
|
return True
|
|
|
|
elif event.type == 'MOUSEMOVE' and (self.press or self.scale or self.rotate):
|
|
#updating only the moving keyframe
|
|
|
|
check_notification(self, text = get_notification_text(self, context.scene.emp), size = 15)
|
|
|
|
if add_distance_on_mouse_change(self, context, event):
|
|
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(len(points)):
|
|
# frame = ((i+3) / 3) + self.avz_frame_start[obj_bonename] - 1
|
|
frame = i + self.avz_frame_start[obj_bonename]
|
|
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]})
|
|
else:
|
|
self.bones_allframes_coords.update({obj_bonename : {frame : points[i]}})
|
|
|
|
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 apply_keyframe_movement(self, context, event, key, coord_3d):
|
|
###Apply the keyframe movement, normally after release ###
|
|
#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
|
|
|
|
update_frames = set()
|
|
for obj_bonename, frames_distance in self.frames_distance.items():
|
|
objname, bonename = 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
|
|
|
|
frames_local_distance = dict()
|
|
for frame in frames_distance.keys():
|
|
if context.scene.emp.channels == 'HEADS':
|
|
local_distance = get_distance(key, self, context, posebone, obj_bonename, frame)
|
|
else:
|
|
local_distance = get_rotation_difference(key, self, context, posebone, obj_bonename, frame)
|
|
|
|
frames_local_distance[frame] = local_distance
|
|
|
|
#apply the coordinates and distance to all the selected keyframes and handles
|
|
mp_update_keyframes(self, context, obj_bonename, frames_local_distance, key) #
|
|
update_frames.update(frames_distance.keys())
|
|
|
|
if context.scene.frame_current_final != self.frame_current:
|
|
context.scene.frame_set(int(self.frame_current), subframe = self.frame_current % 1)
|
|
|
|
update_frames = sorted(update_frames)
|
|
# update_frames = [frame for frames in self.bones_selected_keyframes.values() for frame in frames]
|
|
if update_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 = dict()
|
|
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, context)
|
|
self.update = True
|
|
|
|
context.scene.emp.selected_keyframes = serialize_dict(self.bones_selected_keyframes)
|
|
bpy.ops.ed.undo_push(message = 'Move Motion Path Keyframe')
|
|
|
|
def smooth_motion_path_point(p0, p1, p2, p3, t=0.5):
|
|
"""Catmull-Rom interpolation between p1 and p2"""
|
|
t2 = t * t
|
|
t3 = t2 * t
|
|
|
|
return 0.5 * (
|
|
2.0 * p1 +
|
|
(-p0 + p2) * t +
|
|
(2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2 +
|
|
(-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3
|
|
)
|
|
|
|
def smooth_points(self, context, event):
|
|
'''Running the Smooth Brush'''
|
|
emp = context.scene.emp
|
|
if not emp.smooth:
|
|
if hasattr(self, 'text') and self.text == 'Smooth Brush Mode':
|
|
remove_notification(self, context, text = 'Smooth Brush Mode')
|
|
|
|
return False
|
|
if not self.hover_bone_frame['keyframe']:
|
|
return False
|
|
|
|
#Add the smoothing While pressing and dragging
|
|
obj_bonename, frame = next(iter(self.hover_bone_frame["keyframe"].items()))
|
|
|
|
if event.type == self.select_mouse:
|
|
if event.value == 'PRESS' and not self.smooth:
|
|
self.frames_distance[obj_bonename] = dict()
|
|
initialize_points(self)
|
|
self.smooth = True
|
|
return True
|
|
elif event.value == 'RELEASE'and self.smooth:
|
|
obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self)
|
|
apply_keyframe_movement(self, context, event, key, coord_3d)
|
|
self.smooth = False
|
|
return True
|
|
|
|
if not self.smooth:
|
|
return False
|
|
|
|
frames_n_dist = get_smooth_frames_n_dist(self, context, obj_bonename)
|
|
|
|
# Updating all the points while the mouse is moving and smooth is turned on
|
|
for frame, n_dist in frames_n_dist.items():
|
|
coord = self.bones_keyframes_coords[obj_bonename][frame]
|
|
frames = self.mp_bones_keys[obj_bonename]
|
|
# Use min keyframe in case of negative frames
|
|
first_keyframe = min(self.bones_keyframes_coords[obj_bonename].keys())
|
|
|
|
p1_frame = max(first_keyframe, get_previous_frame(frame, frames))
|
|
p2_frame = get_next_frame(frame, frames)
|
|
p0_frame = max(first_keyframe, get_previous_frame(p1_frame, frames))
|
|
p3_frame = get_next_frame(p2_frame, frames)
|
|
p0_coords = self.bones_keyframes_coords[obj_bonename][p0_frame]
|
|
p1_coords = self.bones_keyframes_coords[obj_bonename][p1_frame]
|
|
p2_coords = self.bones_keyframes_coords[obj_bonename][p2_frame]
|
|
p3_coords = self.bones_keyframes_coords[obj_bonename][p3_frame]
|
|
|
|
initial_coord = self.initial_keyframe_coords[obj_bonename][frame]
|
|
# Using the catmull clarck algorithm
|
|
t = (frame - p1_frame) / (p2_frame - p1_frame)
|
|
|
|
# getting the final smooth point
|
|
smooth_coord = smooth_motion_path_point(p0_coords, p1_coords, p2_coords, p3_coords, t)
|
|
|
|
# Getting the new coordinates based on strength and distance from center
|
|
|
|
new_coord = (smooth_coord - coord) * emp.smooth_strength * (1 - n_dist)
|
|
|
|
i = round(frame - self.avz_frame_start[obj_bonename])
|
|
self.bones_points[obj_bonename][i] += new_coord
|
|
|
|
distance = Vector(self.bones_points[obj_bonename][i] - initial_coord)
|
|
if obj_bonename in self.bones_handles_right:
|
|
#get the distance from the keyframes
|
|
if frame in self.bones_handles_right[obj_bonename]:
|
|
self.bones_handles_right[obj_bonename][frame] = self.initial_handles_right[obj_bonename][frame] + distance
|
|
self.bones_handles_left[obj_bonename][frame] = self.initial_handles_left[obj_bonename][frame] + distance
|
|
|
|
self.frames_distance[obj_bonename][frame] = distance
|
|
|
|
add_distance_to_points(self, obj_bonename, self.frames_distance[obj_bonename])
|
|
self.update = True
|
|
self.area.tag_redraw()
|
|
return True
|
|
|
|
def get_smooth_frames_n_dist(self, context, obj_bonename):
|
|
'''Get all the frames in the range of the smooth brush and their normlized distance'''
|
|
emp = context.scene.emp
|
|
|
|
# Getting the range around the mouse
|
|
radius = emp.radius
|
|
|
|
all_coords2d = np.array(list(self.coords2d_bones_frames.keys()))
|
|
|
|
mouse_pos = np.array([self.mouse_x, self.mouse_y])
|
|
diff = all_coords2d - mouse_pos
|
|
dist = np.linalg.norm(diff, axis=1)
|
|
in_range_mask = dist <= radius
|
|
|
|
points_in_range = all_coords2d[in_range_mask]
|
|
dist_in_range = dist[in_range_mask]
|
|
|
|
# Normalize distances (0 at center, 1 at edge)
|
|
norm_dist = dist_in_range / radius
|
|
|
|
# Get the points that are in range
|
|
points_in_range = all_coords2d[in_range_mask]
|
|
|
|
# Build dict: frame -> normalized distance
|
|
frames_n_dist = {
|
|
self.coords2d_bones_frames[tuple(coord)][obj_bonename]: d
|
|
for coord, d in zip(points_in_range, norm_dist)
|
|
}
|
|
|
|
return frames_n_dist
|
|
|
|
def circle_select_keyframes(self, context, event):
|
|
'''Running the Circle select Brush'''
|
|
emp = context.scene.emp
|
|
if not emp.select_circle:
|
|
return False
|
|
if not any(self.hover_bone_frame.values()):
|
|
return False
|
|
if any((self.press, self.rotate, self.scale)):
|
|
return False
|
|
|
|
#Add the smoothing While pressing and dragging
|
|
|
|
# obj_bonename, frame = next(iter(self.hover_bone_frame["keyframe"].items()))
|
|
key, frame = self.current_hover_frame
|
|
obj_bonename, frame = next(iter(self.hover_bone_frame[key].items()))
|
|
|
|
if event.type == self.select_mouse:
|
|
if event.value == 'PRESS' and not self.select_circle:
|
|
self.select_circle = True
|
|
return True
|
|
# Turn off on release
|
|
elif event.value == 'RELEASE'and self.select_circle:
|
|
get_handles(self, context)
|
|
self.select_circle = False
|
|
return True
|
|
|
|
if not self.select_circle:
|
|
return False
|
|
|
|
frames_n_dist = get_smooth_frames_n_dist(self, context, obj_bonename)
|
|
|
|
for frame in frames_n_dist.keys():
|
|
if self.shift:
|
|
if frame in self.bones_selected_keyframes[obj_bonename]:
|
|
self.bones_selected_keyframes[obj_bonename].remove(frame)
|
|
self.update = True
|
|
|
|
# Remove frame from handles if existing there
|
|
self.bones_handles_left.get(obj_bonename, {}).pop(frame, None)
|
|
self.bones_handles_right.get(obj_bonename, {}).pop(frame, None)
|
|
|
|
else:
|
|
if obj_bonename in self.bones_selected_keyframes:
|
|
if frame not in self.bones_selected_keyframes[obj_bonename]:
|
|
self.bones_selected_keyframes[obj_bonename].append(frame)
|
|
self.update = True
|
|
else:
|
|
self.bones_selected_keyframes.update({obj_bonename : [frame]})
|
|
self.update = True
|
|
|
|
if self.update:
|
|
context.scene.emp.selected_keyframes = serialize_dict(self.bones_selected_keyframes)
|
|
bpy.ops.ed.undo_push(message = 'Add / Remove Keyframe')
|
|
self.area.tag_redraw()
|
|
return True
|
|
|
|
def add_distance_to_cyclic(self, frame, i, obj_bonename, distance):
|
|
|
|
if obj_bonename in self.bones_cyclic:
|
|
return
|
|
if frame == self.mp_bones_keys[obj_bonename][-1]:
|
|
self.bones_points[obj_bonename][i:] = self.initial_bones_points[obj_bonename][i:] + distance
|
|
elif frame == self.mp_bones_keys[obj_bonename][0]:
|
|
self.bones_points[obj_bonename][:i] = self.initial_bones_points[obj_bonename][:i] + distance
|
|
|
|
|
|
def add_distance_on_mouse_change(self, context, event):
|
|
#add the distance to all the selected keyframes or the selected handles
|
|
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(self.region, self.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(self.region, self.region_data, pivot)
|
|
rotation_matrix, angle = get_rotation_2d(self, 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
|
|
self.frames_distance[obj_bonename] = 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])
|
|
|
|
if self.press:
|
|
# Initial coords are from the current frame, and self.initial_coords are where the mouse is hovering
|
|
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(self.region, self.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(self.region, self.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 - initial_coords)
|
|
|
|
self.frames_distance[obj_bonename][frame] = distance
|
|
|
|
#apply the keyframe scaled position
|
|
self.bones_points[obj_bonename][i] = new_coord
|
|
|
|
add_distance_to_cyclic(self, frame, i, obj_bonename, distance)
|
|
|
|
if obj_bonename in self.bones_handles_right:
|
|
#get the distance from the keyframes
|
|
self.bones_handles_right[obj_bonename][frame] = self.initial_handles_right[obj_bonename][frame] + distance
|
|
self.bones_handles_left[obj_bonename][frame] = self.initial_handles_left[obj_bonename][frame] + distance
|
|
|
|
add_distance_to_prop_edit(self, obj_bonename)
|
|
add_distance_to_points(self, obj_bonename, self.frames_distance[obj_bonename])
|
|
|
|
else:
|
|
# distance = loc_coord - Vector(coord_3d)
|
|
distance = loc_coord - self.initial_coord
|
|
|
|
for side in ['r', 'l']:
|
|
move_handle_side(self, obj_bonename, distance, side, key)
|
|
|
|
self.prev_event_value = None
|
|
self.update = True
|
|
self.area.tag_redraw()
|
|
# Tools.redraw_areas(['VIEW_3D'])
|
|
return True
|
|
|
|
def move_handle_side(self, obj_bonename, distance, side, key):
|
|
"""
|
|
Move handles for one side using getattr for dynamic attribute access.
|
|
|
|
Args:
|
|
obj_bonename: Name of the bone object
|
|
distance: Movement distance vector
|
|
side: 'r' for right, 'l' for left
|
|
key: Key parameter for get_side_auto function
|
|
"""
|
|
# Dynamic attribute access using getattr
|
|
handle_coords = getattr(self, f'bones_handles_{["left", "right"][side == "r"]}')[obj_bonename]
|
|
init_handles = getattr(self, f'initial_handles_{["left", "right"][side == "r"]}')[obj_bonename]
|
|
selected_handles = getattr(self, f'bones_selected_handles_{side}')
|
|
opposite_handle_coords = getattr(self, f'bones_handles_{["right", "left"][side == "r"]}')[obj_bonename]
|
|
opposite_init_handles = getattr(self, f'initial_handles_{["right", "left"][side == "r"]}')[obj_bonename]
|
|
|
|
# Side-specific configuration
|
|
auto_side_check = f'handle_{["l", "r"][side == "r"]}'
|
|
|
|
# Check if any frames are selected for this bone and side
|
|
if obj_bonename not in selected_handles:
|
|
return
|
|
|
|
selected_frames = set(selected_handles[obj_bonename])
|
|
|
|
# Process only selected frames
|
|
for frame in selected_frames:
|
|
if frame not in handle_coords:
|
|
continue
|
|
|
|
# Skip if automatic handle should move the other side
|
|
if get_side_auto(self, obj_bonename, frame, key) == auto_side_check:
|
|
continue
|
|
|
|
coord_3d = handle_coords[frame]
|
|
keyframe_coord = self.bones_keyframes_coords[obj_bonename][frame]
|
|
|
|
# Calculate distance length ratio
|
|
try:
|
|
dist_len = abs((init_handles[frame] + distance - Vector(keyframe_coord)).length /
|
|
(init_handles[frame] - Vector(keyframe_coord)).length)
|
|
except ZeroDivisionError:
|
|
dist_len = None
|
|
|
|
# Move the current side handle
|
|
handle_coords[frame] = init_handles[frame] + distance
|
|
|
|
# Handle AUTO type - align opposite handle
|
|
if self.bones_handles_types[obj_bonename][frame] == 'AUTO':
|
|
opposite_handle_coords[frame] = align_handles(
|
|
opposite_init_handles[frame],
|
|
coord_3d,
|
|
Vector(keyframe_coord),
|
|
dist_len
|
|
)
|
|
|
|
if obj_bonename not in self.frames_distance:
|
|
self.frames_distance[obj_bonename] = dict()
|
|
self.frames_distance[obj_bonename][frame] = distance
|
|
|
|
def move_handles_optimized(self, obj_bonename, distance, key):
|
|
"""
|
|
Optimized version using getattr and sets for efficient frame processing.
|
|
"""
|
|
# Move both sides
|
|
for side in ['r', 'l']:
|
|
move_handle_side(self, obj_bonename, distance, side, key)
|
|
|
|
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':
|
|
self.bones_points = copy.deepcopy(self.initial_bones_points)
|
|
update_keyframe_coords(self)
|
|
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.initial_bones_points
|
|
|
|
self.frames_distance = dict()
|
|
self.press = False
|
|
self.scale = False
|
|
self.rotate = False
|
|
self.filter_axis = ''
|
|
self.initial_coord = None
|
|
self.initial_keyframe_coords = None
|
|
self.update = True
|
|
|
|
remove_notification(self, bpy.context)
|
|
|
|
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 get_previous_frame(frame, frames):
|
|
# Getting the previous Previous frame
|
|
prev_i = bisect.bisect_left(frames, frame)
|
|
if prev_i:
|
|
prev_i -= 1
|
|
prev_neighbor = frames[prev_i]
|
|
|
|
return prev_neighbor
|
|
|
|
def get_next_frame(frame, frames):
|
|
# Getting the previous neighbor frame
|
|
next_i = bisect.bisect_right(frames, frame)
|
|
if next_i >= len(frames):
|
|
next_neighbor = frame
|
|
else:
|
|
next_neighbor = frames[next_i]
|
|
|
|
return next_neighbor
|
|
|
|
def edit_prop_frames(self, obj_bonename, frame, prev_k_dist, next_k_dist):
|
|
'''Finds the previous and next last frames of proportional editing (optimized).'''
|
|
emp = bpy.context.scene.emp
|
|
if not emp.prop_edit:
|
|
return None, None
|
|
|
|
frames_coords2d = self.bones_frames_coords2d[obj_bonename]
|
|
if frame not in frames_coords2d:
|
|
return None, None
|
|
|
|
if not hasattr(self, 'prop_editor'):
|
|
self.prop_editor = ProportionalEditor()
|
|
|
|
def reset_frame_points(frame):
|
|
i = round(frame - self.avz_frame_start[obj_bonename])
|
|
self.bones_points[obj_bonename][i] = self.initial_bones_points[obj_bonename][i]
|
|
|
|
frames = np.array(list(frames_coords2d.keys()))
|
|
index = np.where(frames == frame)[0][0]
|
|
|
|
# Precompute 2D coords as array
|
|
coords = np.array([frames_coords2d[f] for f in frames])
|
|
|
|
# Compute Euclidean distance between consecutive frames (vectorized)
|
|
diffs = np.diff(coords, axis=0) # Shape (N-1, 2)
|
|
distances = np.linalg.norm(diffs, axis=1) # Shape (N-1,)
|
|
|
|
# ================================
|
|
# Next neighbors
|
|
# ================================
|
|
total_dist = 0.0
|
|
for i in range(index + 1, len(frames)):
|
|
# If distance is already known, stop early
|
|
if frames[i] in self.frames_distance[obj_bonename]:
|
|
break
|
|
|
|
# Add Euclidean step distance
|
|
total_dist += distances[i - 1] # distance between frames[i-1] and frames[i]
|
|
if total_dist > emp.radius:
|
|
reset_frame_points(frames[i])
|
|
break
|
|
|
|
n_dist = total_dist / emp.radius
|
|
next_k_dist[frames[i]] = (frame, n_dist)
|
|
|
|
# ================================
|
|
# Previous neighbors
|
|
# ================================
|
|
total_dist = 0.0
|
|
for i in range(index - 1, -1, -1):
|
|
if frames[i] in self.frames_distance[obj_bonename]:
|
|
break
|
|
|
|
total_dist += distances[i] # distance between frames[i] and frames[i+1]
|
|
if total_dist > emp.radius:
|
|
reset_frame_points(frames[i])
|
|
break
|
|
|
|
n_dist = total_dist / emp.radius
|
|
prev_k_dist[frames[i]] = (frame, n_dist)
|
|
|
|
return prev_k_dist, next_k_dist
|
|
|
|
|
|
def add_distance_to_prop_edit(self, obj_bonename):
|
|
'''Add distance to proportional editing keyframes and add the keyframes to frames_distance'''
|
|
emp = bpy.context.scene.emp
|
|
if not emp.prop_edit:
|
|
return
|
|
prev_k_dist = dict()
|
|
next_k_dist = dict()
|
|
added_frames_distance = dict()
|
|
frames_distance = self.frames_distance[obj_bonename]
|
|
selected_frames = sorted(frames_distance.keys())
|
|
|
|
for frame in selected_frames:
|
|
edit_prop_frames(self, obj_bonename, frame, prev_k_dist, next_k_dist)
|
|
|
|
# Unify the two dictionaries based on the higher value (The frame that is closer)
|
|
for neighbor_frame in prev_k_dist.keys() | next_k_dist.keys():
|
|
# The dict values are tuples of original frame and normalized distance, we get the tuple with the lowest distance, so the original keyframe
|
|
frame, n_dist = min(prev_k_dist.get(neighbor_frame, (0, 1)), next_k_dist.get(neighbor_frame, (0, 1)), key=lambda x: x[1])
|
|
if not n_dist:
|
|
continue
|
|
interpolated_factor = self.prop_editor.apply_interpolation(n_dist, emp.prop_edit_falloff)
|
|
# Applying the distance to the next keyframe
|
|
distance = frames_distance[frame]
|
|
neighbor_distance = distance * (interpolated_factor)
|
|
|
|
added_frames_distance[neighbor_frame] = neighbor_distance
|
|
|
|
# Add the values to bones points, the actual motion path points
|
|
for added_key, added_dist in added_frames_distance.items():
|
|
added_i = round(added_key - self.avz_frame_start[obj_bonename])
|
|
self.bones_points[obj_bonename][added_i] = self.initial_bones_points[obj_bonename][added_i] + added_dist
|
|
|
|
frames_distance.update(added_frames_distance)
|
|
|
|
|
|
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])
|
|
|
|
if frame_index: # Make sure it's a positive index
|
|
# Get previous keyframe if it was not assigned during prop
|
|
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])
|
|
|
|
#Linear distribution of the points until the previous keyframe
|
|
added_array = np.linspace(prev_dist, distance, prev_frames, endpoint=False)
|
|
|
|
#apply the keyframe position
|
|
if len(self.bones_points[obj_bonename][prev_i :current_i]) != len(added_array):
|
|
continue
|
|
self.bones_points[obj_bonename][prev_i :current_i] = self.initial_bones_points[obj_bonename][prev_i :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 : #and frame in self.bones_selected_keyframes[obj_bonename]
|
|
continue
|
|
next_frames = int(next_key - frame)
|
|
next_i = round(next_key - self.avz_frame_start[obj_bonename])
|
|
|
|
#Linear distribution of the points until the next keyframe
|
|
added_array = np.linspace(distance, Vector((0,0,0)), next_frames, endpoint=False)
|
|
|
|
if len(self.bones_points[obj_bonename][current_i : next_i]) != len(added_array):
|
|
continue
|
|
self.bones_points[obj_bonename][current_i : next_i] = self.initial_bones_points[obj_bonename][current_i : next_i] + added_array
|
|
|
|
def draw_frames_callback_px(self, context):
|
|
'''Drawing keyframe numbers'''
|
|
|
|
font_id = 0 # XXX, need to find out how best to get this.
|
|
alpha = 0.75
|
|
emp = context.scene.emp
|
|
try:
|
|
if context.area != self.area:
|
|
return
|
|
for obj_bonename, keyframes_coords in self.bones_keyframes_coords.items():
|
|
for frame, coord_3d in keyframes_coords.items():
|
|
if frame < self.frame_start[obj_bonename]:
|
|
continue
|
|
|
|
# Checking if it's currently out of range when using around frame range
|
|
if emp.frame_range == 'AROUND':
|
|
current_frame = context.scene.frame_current_final
|
|
before = current_frame - emp.before
|
|
after = current_frame + emp.after
|
|
if frame < before or frame > after-1:
|
|
continue
|
|
|
|
# for region in self.regions:
|
|
coords2d = bpy_extras.view3d_utils.location_3d_to_region_2d(self.region, self.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))) )
|
|
|
|
# self.area.tag_redraw()
|
|
|
|
except ReferenceError:
|
|
return
|
|
|
|
# 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
|
|
|
|
check_frame_change(self, context)
|
|
|
|
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_handles(frame, coords, hover_handle_r, hover_handle_l):
|
|
# Add the handles selection points
|
|
if not emp.handles or not self.bones_handles_right:
|
|
return
|
|
#Add the handles to the selected keyframes
|
|
if frame not in self.bones_handles_right[obj_bonename]:
|
|
return
|
|
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])
|
|
#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}))
|
|
|
|
|
|
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(len(points)):
|
|
|
|
#every 3 points are a vector of a frame
|
|
frame = i + self.avz_frame_start[obj_bonename]
|
|
#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])
|
|
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
|
|
update_handles(frame, coords, hover_handle_r, hover_handle_l)
|
|
|
|
#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)
|
|
|
|
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 = []
|
|
|
|
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 = self.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
|
|
|
|
self.bones_interpolated_colors = dict()
|
|
for obj_bonename, points in self.bones_points.items():
|
|
# Reshaping all the points into array of vectors
|
|
vectors = points.copy()
|
|
# Calculating all the velocities between all the vectors
|
|
velocity_vectors = vectors[1:] - vectors[:-1]
|
|
|
|
# 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 update_prop_edit(self, context, event, emp):
|
|
# Use hotkey to toggle
|
|
if event.type == 'O' and event.value == 'PRESS':
|
|
emp.prop_edit = not emp.prop_edit
|
|
|
|
|
|
if emp.prop_edit and any((self.press, self.rotate, self.scale)):
|
|
key, frame = self.current_hover_frame
|
|
obj_bonename, frame = next(iter(self.hover_bone_frame[key].items()))
|
|
initialize_prop_edit_coord2d(self, context, obj_bonename)
|
|
|
|
|
|
if not emp.prop_edit:
|
|
if hasattr(self, 'text') and self.text == "Proportional Editing Mode":
|
|
remove_notification(self, context, text = "Proportional Editing Mode")
|
|
return False
|
|
|
|
if update_circle_radius(self, context, event, emp):
|
|
if any((self.press, self.rotate, self.scale)):
|
|
add_distance_on_mouse_change(self, context, event)
|
|
# else:
|
|
# self.area.tag_redraw()
|
|
return True
|
|
|
|
return False
|
|
|
|
def update_smooth(self, context, event, emp):
|
|
if self.hover_bone_frame["keyframe"]:
|
|
if self.shift and event.type == 'S' and event.value == 'PRESS':
|
|
emp.smooth = not emp.smooth
|
|
return True
|
|
|
|
if not emp.smooth:
|
|
return False
|
|
|
|
if update_circle_radius(self, context, event, emp):
|
|
# self.update = True
|
|
# context.region.tag_redraw()
|
|
return True
|
|
|
|
return False
|
|
|
|
def update_select_circle(self, context, event, emp):
|
|
if self.hover_bone_frame["keyframe"]:
|
|
if event.type == 'C' and event.value == 'PRESS':
|
|
emp.select_circle = not emp.select_circle
|
|
return True
|
|
|
|
if not emp.select_circle:
|
|
if hasattr(self, 'text') and self.text == "Select Brush Mode":
|
|
remove_notification(self, context, text = "Select Brush Mode")
|
|
return False
|
|
|
|
if update_circle_radius(self, context, event, emp):
|
|
return True
|
|
|
|
return False
|
|
|
|
def update_circle_radius(self, context, event, emp):
|
|
'''Updating radius when F hotkey is pressed'''
|
|
|
|
self.middlemouse = event.type
|
|
region = self.region
|
|
if self.mouse_x != (event.mouse_x - region.x) or self.mouse_y != (event.mouse_y - region.y):
|
|
self.region.tag_redraw()
|
|
|
|
if event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
|
|
if any((self.press, self.rotate, self.scale, self.smooth)):
|
|
if event.type == 'WHEELUPMOUSE':
|
|
emp.radius -= 2
|
|
return True
|
|
elif event.type == 'WHEELDOWNMOUSE':
|
|
emp.radius += 2
|
|
|
|
return True
|
|
|
|
if event.type == 'F' and event.value == 'PRESS' and not self.change_radius:
|
|
# Entering change radius mode
|
|
self.prev_mouse_x = event.mouse_x
|
|
# In case of cancelling
|
|
self.init_radius = emp.radius
|
|
self.change_radius = True
|
|
elif event.type == self.select_mouse and event.value == 'RELEASE' and self.change_radius:
|
|
# Confirm the Radius size
|
|
self.change_radius = False
|
|
del self.prev_mouse_x
|
|
del self.init_radius
|
|
self.area.tag_redraw()
|
|
elif event.type == self.cancel_mouse and event.value == 'RELEASE' and self.change_radius:
|
|
# Cancel the Radius size
|
|
emp.radius = self.init_radius
|
|
self.change_radius = False
|
|
del self.prev_mouse_x
|
|
del self.init_radius
|
|
self.update = True
|
|
self.region.tag_redraw()
|
|
|
|
if not self.change_radius:
|
|
return False
|
|
|
|
if self.prev_mouse_x > event.mouse_x:
|
|
emp.radius -= 2
|
|
|
|
if self.prev_mouse_x < event.mouse_x:
|
|
emp.radius += 2
|
|
|
|
self.prev_mouse_x = event.mouse_x
|
|
|
|
|
|
return True
|
|
|
|
def draw_circle(self, context):
|
|
"""Create a 2D circle batch"""
|
|
# Drawing only during keyframe operation
|
|
try:
|
|
emp = context.scene.emp
|
|
if not any({emp.prop_edit, emp.smooth, emp.select_circle}):
|
|
return
|
|
|
|
if self.middlemouse == 'MIDDLEMOUSE':
|
|
return
|
|
|
|
if emp.prop_edit and any((self.press, self.rotate, self.scale)):
|
|
circle_x = self.prop_edit_mouse[0]
|
|
circle_y =self.prop_edit_mouse[1]
|
|
else:
|
|
circle_x = self.mouse_x
|
|
circle_y = self.mouse_y
|
|
|
|
pref = context.preferences.addons[__package__].preferences
|
|
if any((self.press, self.rotate, self.scale, self.smooth, self.change_radius)):
|
|
color = pref.mp_brush_active
|
|
elif self.hover_bone_frame.get('keyframe') is not None:
|
|
color = pref.mp_brush_hover
|
|
else:
|
|
color = pref.mp_brush_disabled
|
|
|
|
segments=16
|
|
|
|
vertices = []
|
|
for i in range(segments+1):
|
|
angle = 2.0 * math.pi * i / segments
|
|
x = circle_x + emp.radius * math.cos(angle)
|
|
y = circle_y + emp.radius * math.sin(angle)
|
|
vertices.append((x, y))
|
|
|
|
self.circle_shader = gpu.shader.from_builtin('UNIFORM_COLOR')
|
|
self.circle_batch = batch_for_shader(self.circle_shader, 'LINE_LOOP', {"pos": vertices})
|
|
|
|
# Enable blending and set line width
|
|
gpu.state.blend_set('ALPHA')
|
|
gpu.state.line_width_set(2.0)
|
|
|
|
# Draw circle
|
|
self.circle_shader.bind()
|
|
self.circle_shader.uniform_float("color", color)
|
|
self.circle_batch.draw(self.circle_shader)
|
|
|
|
except ReferenceError:
|
|
remove_draw_circle()
|
|
return
|
|
|
|
def draw_notification_callback(self, context):
|
|
'''Write a text notification during modal operators'''
|
|
|
|
font_id = 0 # need to find out how best to get this.
|
|
as_height = Tools.asset_shelf_height(context)
|
|
# draw some text
|
|
blf.position(font_id, 70, 20 + as_height, 0)
|
|
blf.size(font_id, self.size)
|
|
blf.color(0, 1, 1, 0, 1)
|
|
|
|
blf.draw(font_id, self.text)
|
|
|
|
|
|
def remove_notification(self, context, text = None):
|
|
''' Check if notification was on and remove it '''
|
|
if not hasattr(self, 'draw_notification'):
|
|
return
|
|
emp = context.scene.emp
|
|
# Instead of removing switch to a new mode
|
|
if any({emp.prop_edit, emp.smooth, emp.select_circle}) and emp.motion_path:
|
|
self.text = get_notification_text(self, emp)
|
|
self.size = 20
|
|
self.area.tag_redraw()
|
|
return
|
|
|
|
if not hasattr(self, 'text'):
|
|
return
|
|
if text is None or self.text == text:
|
|
bpy.types.SpaceView3D.draw_handler_remove(self.draw_notification, 'WINDOW')
|
|
del self.draw_notification, self.text, self.size
|
|
self.area.tag_redraw()
|
|
|
|
def check_notification(self, text, size = 15):
|
|
'''Check if notification already exist, if yes then just update text'''
|
|
self.text = text
|
|
self.size = size
|
|
|
|
if not hasattr(self, 'draw_notification') or not hasattr(self, 'text'):
|
|
self.draw_notification = bpy.types.SpaceView3D.draw_handler_add(draw_notification_callback, (self, bpy.context), 'WINDOW', 'POST_PIXEL')
|
|
|
|
#self.area.tag_redraw()
|
|
|
|
def get_notification_text(self,emp):
|
|
# print('get notification text')
|
|
|
|
mapping = {
|
|
"Move Keyframes ": self.press,
|
|
"Rotate Keyframes ": self.rotate,
|
|
"Scale Keyframes ": self.scale,
|
|
"Proportional Editing Mode": emp.prop_edit,
|
|
"Smooth Brush Mode": emp.smooth,
|
|
"Select Brush Mode": emp.select_circle
|
|
}
|
|
|
|
text = next(key for key, value in mapping.items() if value)
|
|
|
|
return text
|
|
|
|
def remove_draw_circle():
|
|
dns = bpy.app.driver_namespace
|
|
if 'emp_draw_circle' in dns:
|
|
draw_prop_edit_handler = dns['emp_draw_circle']
|
|
bpy.types.SpaceView3D.draw_handler_remove(draw_prop_edit_handler, 'WINDOW')
|
|
dns.pop('emp_draw_circle')
|
|
|
|
def add_remove_draw_circle(self, context):
|
|
"""Start the mouse circle drawing"""
|
|
emp = context.scene.emp
|
|
dns = bpy.app.driver_namespace
|
|
|
|
should_draw = any({emp.prop_edit, emp.smooth, emp.select_circle})
|
|
if self.draw_circle is None and should_draw:
|
|
remove_draw_circle()
|
|
# Add draw handler for 2D drawing
|
|
self.draw_circle = bpy.types.SpaceView3D.draw_handler_add(
|
|
draw_circle, (self, context, ), 'WINDOW', 'POST_PIXEL'
|
|
)
|
|
check_notification(self, text = get_notification_text(self, emp), size = 20)
|
|
# Tools.notification_invoke(self, context, text = get_notification_text(emp), size = 20)
|
|
dns['emp_draw_circle'] = self.draw_circle
|
|
self.area.tag_redraw()
|
|
|
|
elif self.draw_circle is not None and not should_draw:
|
|
remove_draw_circle()
|
|
self.draw_circle = None
|
|
self.area.tag_redraw()
|
|
if not emp.smooth:
|
|
self.smooth = False
|
|
|
|
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
|
|
|
|
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'''
|
|
dns = bpy.app.driver_namespace
|
|
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:
|
|
dns.pop('mp_dh')
|
|
if 'mp_df' in bpy.app.driver_namespace:
|
|
dns.pop('mp_df')
|
|
|
|
remove_draw_circle()
|
|
|
|
self.mp_vis = False
|
|
|
|
# Operator with modal and draw handler
|
|
class MotionPathOperator(bpy.types.Operator):
|
|
|
|
"""Creates an editable motion path, Ctrl click to refresh"""
|
|
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
|
|
emp = scene.emp
|
|
|
|
emp.refresh = False
|
|
if emp.motion_path:
|
|
if event.ctrl:
|
|
# Refresh the motion path
|
|
emp.refresh = True
|
|
else:
|
|
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.area = context.area
|
|
self.region = context.region
|
|
self.region_data = context.region_data
|
|
# Create multiple regions incase of multiple viewports for the frame number drawing
|
|
self.regions = []
|
|
self.window = context.window
|
|
|
|
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 = ''
|
|
self.smooth = False
|
|
self.select_circle = False
|
|
self.change_radius = False
|
|
|
|
region = context.region
|
|
self.mouse_x = event.mouse_x - region.x
|
|
self.mouse_y = event.mouse_y - region.y
|
|
|
|
# Draw only during update
|
|
self.update = False
|
|
self.channels = emp.channels if self.posemode else 'HEADS'
|
|
self.cursor_offset = emp.cursor_offset
|
|
self.camera_space = emp.camera_space
|
|
|
|
# Using this variable to update the draw only once during hover
|
|
self.current_hover_frame = (None, None)
|
|
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
|
|
|
|
#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.prev_event_value = None # records the previous mouse event to see if it was just a click and release for selecting
|
|
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.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
|
|
|
|
# Using this to compare with the properties if they changed
|
|
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()
|
|
# Points in camera space
|
|
self.camspace_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
|
|
#Dictionary to use the frames and distance from initial coords per bone/object
|
|
self.frames_distance = dict()
|
|
#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.draw_circle = None
|
|
# 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
|
|
# Notify that motion path is turned on in case of using temp ctrls
|
|
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
|
|
|
|
get_cyclic_modifiers(self, context, obj, obj_bonename)
|
|
|
|
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, context)
|
|
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
|
|
|
|
#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
|
|
self.area.tag_redraw()
|
|
# Tools.redraw_areas(['VIEW_3D'])
|
|
|
|
#assign the draw handler
|
|
context.window_manager.modal_handler_add(self)
|
|
|
|
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 emp.refresh:
|
|
get_mp_points(self, context)
|
|
emp.refresh = False
|
|
return {'PASS_THROUGH'}
|
|
|
|
if not self.mp_vis:
|
|
check_mp_vis_on(self, context)
|
|
return {'PASS_THROUGH'}
|
|
|
|
# if check_removed_items(self, context, emp):
|
|
# return {'FINISHED'}
|
|
|
|
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)
|
|
|
|
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 not self.find_area_under_mouse(context, event):
|
|
return {'PASS_THROUGH'}
|
|
|
|
if update_prop_edit(self, context, event, emp):
|
|
return{'RUNNING_MODAL'}
|
|
if update_smooth(self, context, event, emp):
|
|
return{'RUNNING_MODAL'}
|
|
if update_select_circle(self, context, event, emp):
|
|
return{'RUNNING_MODAL'}
|
|
|
|
region = self.region
|
|
self.mouse_x = event.mouse_x - region.x
|
|
self.mouse_y = event.mouse_y - region.y
|
|
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
|
|
self.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)
|
|
self.area.tag_redraw()
|
|
return{'PASS_THROUGH'}
|
|
|
|
add_remove_draw_circle(self, context)
|
|
|
|
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 circle_select_keyframes(self, context, event):
|
|
return {'RUNNING_MODAL'}
|
|
|
|
if smooth_points(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, context)
|
|
return {'PASS_THROUGH'}
|
|
|
|
self.current_selected_items = context.selected_pose_bones
|
|
return {'PASS_THROUGH'}
|
|
|
|
anim_update = False
|
|
|
|
mp_handles_on_off(self, context)
|
|
|
|
if mp_set_handle_type(self, context, event):
|
|
return {'PASS_THROUGH'}
|
|
mp_set_interpolation(self, context, event)
|
|
check_property_change(self, context)
|
|
|
|
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)
|
|
shift_icon = "EVENT_SHIFT" if bpy.app.version < (4, 3, 0) else "KEY_SHIFT"
|
|
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.separator()
|
|
row.label(text=" Proportional Editing", icon="EVENT_O")
|
|
elif context.scene.emp.smooth:
|
|
row.label(text="Smooth brush ")
|
|
row.label(text=" Change Radius", icon="EVENT_F")
|
|
row.separator()
|
|
row.label(icon = shift_icon)
|
|
row.label(text=" Turn Off Smooth Keyframes", icon="EVENT_S")
|
|
elif context.scene.emp.select_circle:
|
|
row.label(text="Select brush ")
|
|
row.label(text=" Select Keyframes", icon="MOUSE_LMB")
|
|
row.separator()
|
|
row.label(text=" Remove Selection", icon=shift_icon)
|
|
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=" Change Radius", icon="EVENT_F")
|
|
row.separator()
|
|
row.label(text=" Turn Off Select brush", icon="EVENT_C")
|
|
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=shift_icon)
|
|
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=" Proportional Editing", icon="EVENT_O")
|
|
row.label(icon=shift_icon)
|
|
row.label(text=" Smooth Keyframes", icon="EVENT_S")
|
|
row.separator()
|
|
if bpy.app.version >= (4, 2, 0):
|
|
row.label(text=" Zoom Keyframes", icon="EVENT_PERIOD")
|
|
|
|
context.workspace.status_text_set(draw_status)
|
|
|
|
def update_context(self, context):
|
|
|
|
# Update context if it becomes invalid (workspace switch)
|
|
if context.area == self.area and context.region == self.region:
|
|
return
|
|
|
|
# Try to find a valid 3D viewport in the current context
|
|
for area in context.screen.areas:
|
|
if area.type == 'VIEW_3D':
|
|
self.area = area
|
|
for region in area.regions:
|
|
if region.type == 'WINDOW':
|
|
self.region = region # Get the main region
|
|
self.region_data = region.data
|
|
return
|
|
|
|
def find_area_under_mouse(self, context, event):
|
|
"""Find the 3D viewport area and region under the mouse cursor."""
|
|
# if event.type != 'MOUSEMOVE':
|
|
# return True
|
|
wm = context.window_manager
|
|
mouse_x, mouse_y = event.mouse_x, event.mouse_y
|
|
for window in wm.windows:
|
|
if not window.screen:
|
|
continue
|
|
# Check bounds
|
|
if not (0 <= mouse_x < window.x + window.width and 0 <= mouse_y < window.y + window.height):
|
|
continue
|
|
|
|
# Find area under mouse
|
|
for area in window.screen.areas:
|
|
if area.type != 'VIEW_3D':
|
|
continue
|
|
if not (area.x <= mouse_x < area.x + area.width and area.y <= mouse_y < area.y + area.height):
|
|
continue
|
|
# Find the main region
|
|
for region in area.regions:
|
|
if region.type != 'WINDOW':
|
|
continue
|
|
# if 0 <= region_x < region.width and 0 <= region_y < region.height:
|
|
if region.x <= mouse_x <= (region.x + region.width) or region.y <= mouse_y <= (region.y + region.height):
|
|
self.area = area
|
|
self.region = region # Get the main region
|
|
self.region_data = region.data
|
|
return True
|
|
|
|
return False
|
|
|
|
def check_frame_change(self, context):
|
|
# Checking for frame change, updating here because modal operator is not updated during frame change
|
|
if self.frame_current == context.scene.frame_current_final:
|
|
return
|
|
|
|
self.frame_current = context.scene.frame_current_final
|
|
|
|
if self.camera_space:
|
|
keyframe_coords = self.bones_keyframes_coords.copy()
|
|
for obj_bonename in self.mp_bone_names:
|
|
add_camera_space(self, context, obj_bonename)
|
|
|
|
update_keyframe_coords(self)
|
|
|
|
# updating handles offset
|
|
for obj_bonename, frames_coords in self.bones_handles_left.items():
|
|
for frame, coords in frames_coords.items():
|
|
offset = Vector(self.bones_keyframes_coords[obj_bonename][frame] - keyframe_coords[obj_bonename][frame])
|
|
coords += offset
|
|
self.bones_handles_right[obj_bonename][frame] += offset
|
|
|
|
coords_2d_update(self, context)
|
|
|
|
self.update = True
|
|
|
|
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 ShowMessageBox(message = "", title = "", icon = 'INFO'):
|
|
|
|
def draw(self, context):
|
|
self.layout.label(text=message)
|
|
|
|
bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
|
|
|
|
def check_property_change(self, context):
|
|
'''Check for different property changes'''
|
|
|
|
properties = ['channels', 'cursor_offset', 'camera_space']
|
|
for prop in properties:
|
|
prop_val = getattr(context.scene.emp, prop)
|
|
if getattr(self, prop) == prop_val:
|
|
continue
|
|
|
|
if not self.posemode and context.scene.emp.channels != self.channels:
|
|
context.scene.emp.channels = 'HEADS'
|
|
self.area.tag_redraw()
|
|
ShowMessageBox(message = "Motion path rotations are only available for Armature bones",
|
|
title = "Rotation Channels")
|
|
return
|
|
|
|
setattr(self, prop, prop_val)
|
|
# self.cursor_offset = context.scene.emp.cursor_offset
|
|
get_mp_points(self, context)
|
|
update_keyframe_coords(self)
|
|
removed_selected_unknown_keyframes(self)
|
|
get_handles(self, context)
|
|
calculate_velocities(self, context)
|
|
coords_2d_update(self, context)
|
|
self.update = True
|
|
return
|
|
|
|
|
|
def removed_selected_unknown_keyframes(self):
|
|
'''Finding keyframes that are selected and doesn't exist, relevant when changing channels for exampls'''
|
|
for obj_bonename, frames in self.bones_selected_keyframes.items():
|
|
removed_keyframes = set(frames).difference(set(self.mp_bones_keys[obj_bonename]))
|
|
for frame in removed_keyframes:
|
|
self.bones_selected_keyframes[obj_bonename].remove(frame)
|
|
|
|
def quit_mp(self, context):
|
|
|
|
# context.scene.emp.prop_edit = False
|
|
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
|
|
if bonename not in obj.pose.bones:
|
|
continue
|
|
bone = obj.pose.bones[bonename]
|
|
if 'atb_mp_name' in bone.keys():
|
|
del bone['atb_mp_name']
|
|
if 'mp_cursor_offset' in bone.keys():
|
|
del bone['mp_cursor_offset']
|
|
|
|
#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()
|
|
|
|
remove_notification(self, context)
|
|
|
|
# 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)
|
|
update_keyframe_coords(self)
|
|
#get current bones keys to avoid recalculation of path points
|
|
self.current_bones_keys = get_current_bones_keys(self, context)
|
|
interpolation = None
|
|
self.update = True
|
|
|
|
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 False
|
|
update_mp_points(self, context, frames)
|
|
calculate_velocities(self, context)
|
|
update_keyframe_coords(self)
|
|
#get current bones keys to avoid recalculation of path points
|
|
self.current_bones_keys = get_current_bones_keys(self, context)
|
|
handle_type = None
|
|
self.update = True
|
|
return True
|
|
|
|
if event.type != 'V' or event.value != 'PRESS':
|
|
return False
|
|
if not self.bones_selected_keyframes:
|
|
return 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_handle_type_menu, title = 'Set Keyframe Handle Type')
|
|
return True
|
|
|
|
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.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 self.frame_range == 'MANUAL':
|
|
update_manual_frame_range(self, context)
|
|
return
|
|
|
|
# If motion path is not turned on then skip it
|
|
if not emp.motion_path:
|
|
return
|
|
|
|
clear_frame_range_owner()
|
|
|
|
global frame_range_update
|
|
|
|
# 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
|
|
self.bones_points[obj_bonename] = self.bones_points[obj_bonename][remove_points:]
|
|
update_keyframe_coords(self)
|
|
|
|
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]
|
|
self.bones_points[obj_bonename] = self.bones_points[obj_bonename][:-remove_points]
|
|
update_keyframe_coords(self)
|
|
|
|
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)
|
|
|
|
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):
|
|
'''Updating the list of motion path keyframes in a dictionary'''
|
|
|
|
transform = get_transform_channel(self.posemode, bpy.context, item)
|
|
|
|
frames_with_offset = map(lambda frame: round(add_frame_offset(item.id_data, frame)), get_bone_keyframes(item, transform)[::2])
|
|
frames_with_offset = sorted(set(frames_with_offset))
|
|
|
|
#Limiting the frames inside manual range
|
|
if self.frame_range_type == 'MANUAL':
|
|
frame_start, frame_end = self.frame_start[obj_bonename], self.frame_end[obj_bonename]
|
|
frames_with_offset = [frame for frame in frames_with_offset if frame_start <= frame <= frame_end]
|
|
|
|
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
|
|
end_repeat = end + length * r
|
|
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 + self.avz_frame_start[obj_bonename]
|
|
frame_end_repeat = end_repeat + self.avz_frame_start[obj_bonename]
|
|
|
|
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)
|
|
end_adjust = round(frame_end_repeat - adjusted_end)
|
|
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 get_cyclic_modifiers(self, context, obj, obj_bonename):
|
|
|
|
objname, bonename = obj_bonename
|
|
fcurves = Tools.get_fcurves(obj, obj.animation_data.action)
|
|
path, array_len = get_fcu_path(context, self.posemode, obj, bonename)
|
|
|
|
modifiers = []
|
|
#get all the cyclic modifiers
|
|
for i in range(array_len):
|
|
fcu = fcurves.find(data_path = path, index = i)
|
|
if fcu is None:
|
|
continue
|
|
if not len(fcu.modifiers):
|
|
continue
|
|
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:
|
|
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:
|
|
return
|
|
#Currently including only none and repeat
|
|
if mod.mode_before not in {'NONE', 'REPEAT'} or mod.mode_after not in {'NONE', 'REPEAT'}:
|
|
return
|
|
|
|
modifiers.append(mod)
|
|
|
|
self.bones_cyclic.update({obj_bonename : modifiers})
|
|
# if modifiers:# and available_for_update(modifiers):
|
|
# self.bones_cyclic.update({obj_bonename : modifiers})
|
|
# elif obj_bonename in self.bones_cyclic:
|
|
# del self.bones_cyclic[obj_bonename]
|
|
|
|
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 = []
|
|
# self.bones_cyclic = 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(obj, obj.animation_data.action)
|
|
path, array_len = get_fcu_path(context, self.posemode, obj, bonename)
|
|
|
|
#get all the cyclic modifiers
|
|
for i in range(array_len):
|
|
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 True
|
|
#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 True
|
|
#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 True
|
|
|
|
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})
|
|
|
|
self.bones_cyclic.update({obj_bonename : modifiers})
|
|
# if modifiers:
|
|
#
|
|
# elif obj_bonename in self.bones_cyclic:
|
|
# del self.bones_cyclic[obj_bonename]
|
|
|
|
return False
|
|
|
|
def restore_points_before_cyclic():
|
|
# Moving points back to the initial coords before adding cyclic
|
|
if key != 'keyframe':
|
|
return
|
|
if obj_bonename not in self.bones_cyclic:
|
|
return
|
|
frames_negative_dist = dict()
|
|
|
|
for frame, distance in self.frames_distance[obj_bonename].items():
|
|
restore_point = round(frame - self.avz_frame_start[obj_bonename])
|
|
# if obj_bonename in self.bones_cyclic:
|
|
#restoring previous points before repeating them in cyclic fcurves back to the initial coordinates
|
|
self.bones_points[obj_bonename][restore_point] -= distance
|
|
|
|
# Creates a copy of frame distance going backward
|
|
frames_negative_dist.update({frame : distance * -1})
|
|
|
|
add_distance_to_points(self, obj_bonename, frames_negative_dist)
|
|
|
|
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)
|
|
for frame in frames:
|
|
|
|
prev_keyframe = get_previous_frame(frame, keyframes)
|
|
|
|
#get frames that are pre first keyframe
|
|
if frame < keyframes[0]:
|
|
prev_keyframe = frame
|
|
|
|
next_keyframe = get_next_frame(frame, keyframes)
|
|
|
|
frame_range.append(prev_keyframe)
|
|
frame_range.append(next_keyframe)
|
|
|
|
return frame_range
|
|
|
|
def add_distance_before_after(self, obj_bonename):
|
|
'''Add distance before the first frame and after the last keyframe'''
|
|
if key in {'handle_r', 'handle_l'}:
|
|
return
|
|
|
|
if obj_bonename not in self.mp_bones_keys:
|
|
return
|
|
|
|
if obj_bonename in self.bones_cyclic:
|
|
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])
|
|
coord = self.bones_points[obj_bonename][restore_point]
|
|
# 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-1:
|
|
# 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-1]
|
|
self.bones_points[obj_bonename][restore_point-1] = 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 #distance_array
|
|
|
|
if frame == last:
|
|
#adding distance to all frames after the last keyframe
|
|
remaining_points = self.bones_points[obj_bonename][restore_point+1:]
|
|
if len(remaining_points) > 0:
|
|
# length = len(remaining_points)#//3
|
|
# distance_array = np.tile(np.array(distance), round(length))
|
|
# remaining_points += distance_array
|
|
remaining_points += distance
|
|
|
|
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 = []
|
|
# Storing previous status of modifiers for comparison
|
|
prev_modifiers = self.bones_cyclic.get(obj_bonename)
|
|
i_keyframes = dict()
|
|
|
|
if get_i_keyframe_and_modifiers():
|
|
return
|
|
|
|
#if it's only partly cyclic then recalculate the whole motion path
|
|
if not available_for_update(modifiers) or modifiers != prev_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
|
|
|
|
# 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)
|
|
|
|
# cyclic_points = copy.deepcopy(self.bones_points)
|
|
#creating a copy for cyclic points to be used after the distance array was added to the first and last keys
|
|
if hasattr(self, 'initial_bones_points'):
|
|
stored_bones_points = self.initial_bones_points
|
|
else:
|
|
stored_bones_points = self.bones_points.copy()
|
|
|
|
# Storing also the camera space coordinates separatly
|
|
if self.camera_space:
|
|
stored_cam_space = self.camspace_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]
|
|
end_p = avz_mp.frame_end - self.avz_frame_start[obj_bonename]
|
|
|
|
if bpy.app.version >= (4, 1, 0) :
|
|
end_p += 1
|
|
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]
|
|
|
|
if self.camera_space:
|
|
stored_cam_start = stored_cam_space[obj_bonename][:start_p]
|
|
stored_cam_end = stored_cam_space[obj_bonename][end_p:]
|
|
self.camspace_points[obj_bonename] = np.concatenate((stored_cam_start, self.camspace_points[obj_bonename], stored_cam_end))
|
|
add_camera_space(self, context, obj_bonename)
|
|
else:
|
|
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))
|
|
|
|
difference = self.bones_points[obj_bonename][start_p : end_p] - stored_bones_points[obj_bonename][start_p : end_p]
|
|
|
|
add_distance_before_after(self, obj_bonename)
|
|
|
|
#skip if not cyclic
|
|
if not obj_bonename in self.bones_cyclic or fr_update:
|
|
continue
|
|
if not len(self.bones_cyclic[obj_bonename]):
|
|
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 = self.bones_cyclic[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])
|
|
restrict_end_point = round(mod.frame_end - self.avz_frame_start[obj_bonename]) + 1
|
|
|
|
#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]) + 1
|
|
end_after_cyclic = round(start_after_cyclic + length * (repeat_after -1 )) + 1
|
|
|
|
end_before_cyclic = round(start_key - self.avz_frame_start[obj_bonename])
|
|
start_before_cyclic = round(end_before_cyclic - length * (repeat_before))
|
|
|
|
#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)
|
|
# cyclic_points
|
|
self.bones_points[obj_bonename][start_after_cyclic : end_after_cyclic] = stored_bones_points[obj_bonename][start_after_cyclic : end_after_cyclic]
|
|
repeat_mp_difference(self, obj_bonename, repeat_after, difference[1:], length, start_p + 1, 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] = stored_bones_points[obj_bonename][start_before_cyclic : end_before_cyclic]
|
|
repeat_mp_difference(self, obj_bonename, repeat_before, difference[:-1], -length, start_p, end_p -1, mod)
|
|
|
|
#update the handles
|
|
get_handles(self, context)
|
|
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
|
|
emp = context.scene.emp
|
|
#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 current frame is on subframe then switch to to a rounded frame, otherwise
|
|
# Blender will create the motion path on one step from this subframe
|
|
if context.scene.frame_subframe:
|
|
context.scene.frame_set(round(context.scene.frame_current))
|
|
|
|
#if the bone selection is changed 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:
|
|
select_bone(obj, bonename, 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:
|
|
select_bone(obj, bonename, 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
|
|
#Always using MANUAL because we are sampling every time separate chuncks
|
|
range_type = 'MANUAL' if bpy.app.version >= (4, 0, 0) else 'SCENE'
|
|
|
|
avz_mp = obj.pose.animation_visualization.motion_path if self.posemode else obj.animation_visualization.motion_path
|
|
|
|
# Works only from blender 4.2
|
|
if hasattr(avz_mp, 'use_camera_space_bake'):
|
|
if avz_mp.use_camera_space_bake != emp.camera_space :
|
|
avz_mp.use_camera_space_bake = emp.camera_space
|
|
|
|
if self.posemode:
|
|
bpy.ops.pose.paths_calculate(range = range_type, bake_location = emp.channels)
|
|
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.reshape(-1, 3)})
|
|
|
|
update_camera_space(self, context, obj, obj_bonename)
|
|
|
|
add_cursor_offset(self, context, obj_bonename, item)
|
|
#get the frames from the keyframes sorted
|
|
update_mp_bones_keys(self, item, obj_bonename)
|
|
|
|
if self.posemode:
|
|
bpy.ops.pose.paths_clear(only_selected=False)
|
|
else:
|
|
bpy.ops.object.paths_clear(only_selected=False)
|
|
|
|
#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:
|
|
select_bone(obj, bonename, 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:
|
|
select_bone(obj, bonename, 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)
|
|
|
|
|
|
#If it was originally on subframe then move back to the subframe
|
|
if self.frame_current != context.scene.frame_current_final:
|
|
context.scene.frame_set(int(self.frame_current), subframe = self.frame_current %1)
|
|
|
|
|
|
def select_bone(obj, bonename, select: bool):
|
|
|
|
bone = obj.data.bones[bonename] if bpy.app.version < (5, 0, 0) else obj.pose.bones[bonename]
|
|
bone.select = select
|
|
|
|
def check_camera_space(self, context, emp):
|
|
'''Switch to camera space once it's turned on'''
|
|
|
|
if self.camera_space == emp.camera_space:
|
|
return False
|
|
if not context.scene.camera:
|
|
return False
|
|
|
|
self.camera_space = emp.camera_space
|
|
get_mp_points(self, context)
|
|
update_keyframe_coords(self)
|
|
coords_2d_update(self, context)
|
|
self.update = True
|
|
return True
|
|
|
|
def add_camera_space(self, context, obj_bonename):
|
|
# Adding the camera matrix to the vectors
|
|
cam = context.scene.camera
|
|
|
|
# Adding the camera matrix to the vectors
|
|
M_cam = np.array(cam.matrix_world, dtype=np.float32)
|
|
R = np.array(M_cam[:3, :3], dtype=np.float32)
|
|
T = np.array(M_cam[:3, 3], dtype=np.float32)
|
|
|
|
self.bones_points[obj_bonename] = self.camspace_points[obj_bonename] @ R.T + T
|
|
|
|
def update_camera_space(self, context, obj, obj_bonename):
|
|
|
|
# Switching to camera space
|
|
avz_mp = obj.pose.animation_visualization.motion_path if self.posemode else obj.animation_visualization.motion_path
|
|
if not hasattr(avz_mp, 'use_camera_space_bake'):
|
|
return
|
|
if not avz_mp.use_camera_space_bake:
|
|
return
|
|
|
|
# Storing the original camera space points
|
|
self.camspace_points[obj_bonename] = self.bones_points[obj_bonename].copy()
|
|
|
|
add_camera_space(self, context, obj_bonename)
|
|
|
|
|
|
def matrix_differece(self, context):
|
|
'''get the difference between the world matrix and the basis matrix in local space, for converting point distance to keyframes distance'''
|
|
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):
|
|
'''get the distance between the initial coordinates and the current one and convert to local 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)
|
|
|
|
matrix_diff = self.matrix_diff[obj_bonename]
|
|
|
|
# Converting the distance loc_coord - self.initial_coord to local space distance
|
|
distance = (parent_matrix.inverted() @ matrix_diff.inverted() @ loc_coord) - (parent_matrix.inverted() @ matrix_diff.inverted() @ initial_coord)
|
|
|
|
return distance
|
|
|
|
def get_rotation_difference(key, self, context, pb, obj_bonename, frame):
|
|
'''Get the difference of the bone rotation vectors and convert to a rotation difference'''
|
|
obj = pb.id_data
|
|
scene = context.scene
|
|
|
|
if key == 'keyframe':
|
|
loc_coord = Vector(self.bones_keyframes_coords[obj_bonename][frame])
|
|
|
|
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
|
|
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
|
|
frame = self.bones_handles_frames[obj_bonename][frame][0]
|
|
|
|
if frame != context.scene.frame_current_final:
|
|
subframe, new_frame = math.modf(frame)
|
|
|
|
scene.frame_set(int(new_frame), subframe = subframe)
|
|
|
|
# --- step 1: target orientation ---
|
|
|
|
head = obj.matrix_world @ pb.head
|
|
# Adding the offset from the cursor offset in case it was turned on
|
|
if 'mp_cursor_offset' in pb.keys():
|
|
head += Vector(pb['mp_cursor_offset'])
|
|
|
|
target = (loc_coord - head).normalized()
|
|
|
|
bone_world = obj.matrix_world @ pb.matrix
|
|
y_axis = (bone_world.to_3x3() @ Vector((0,1,0))).normalized()
|
|
|
|
q = y_axis.rotation_difference(target)
|
|
|
|
target_world = q.to_matrix().to_4x4() @ bone_world
|
|
target_world.translation = head
|
|
target_pose = obj.matrix_world.inverted() @ target_world
|
|
|
|
# --- step 2: delta rotation ---
|
|
|
|
parent_matrix = pb.parent.matrix if pb.parent else Matrix.Identity(4)
|
|
matrix_diff = self.matrix_diff[obj_bonename]
|
|
|
|
|
|
current_rot = parent_matrix.inverted() @ matrix_diff.inverted() @ pb.matrix
|
|
target_rot = parent_matrix.inverted() @ matrix_diff.inverted() @ target_pose
|
|
|
|
if len(pb.rotation_mode) == 3:
|
|
current_rot = Vector(current_rot.to_euler(pb.rotation_mode))
|
|
target_rot = Vector(target_rot.to_euler(pb.rotation_mode))
|
|
else:
|
|
current_rot = current_rot.to_quaternion()
|
|
target_rot = target_rot.to_quaternion()
|
|
|
|
# Get the difference between the rotation to the current one
|
|
diff_rot = target_rot - current_rot
|
|
|
|
return diff_rot
|
|
|
|
|
|
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(obj, obj.animation_data.action)
|
|
path, array_len = get_fcu_path(context, self.posemode, obj, bonename)
|
|
|
|
for i in range(array_len):
|
|
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])
|
|
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]
|
|
|
|
fcurves = Tools.get_fcurves(obj, obj.animation_data.action)
|
|
path, array_len = get_fcu_path(context, self.posemode, obj, bonename)
|
|
|
|
for i in range(array_len):
|
|
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))
|
|
# if frame not in self.bones_selected_keyframes[obj_bonename]:
|
|
if frame not in frame_distance.keys():
|
|
continue
|
|
keyframes.add(frame)
|
|
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)
|
|
missing_frames = set(frame_distance.keys()).difference(keyframes)
|
|
if not missing_frames:
|
|
continue
|
|
for missing_frame in missing_frames:
|
|
value = fcu.evaluate(missing_frame)
|
|
if key == 'keyframe':
|
|
value += frame_distance[missing_frame][i]
|
|
|
|
# Reverting to the real frame withou offset before adding the keyframe
|
|
missing_frame = add_frame_offset(obj, missing_frame, add = True)
|
|
keyframe_point = fcu.keyframe_points.insert(missing_frame, value)
|
|
|
|
fcurves.update()
|
|
|
|
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):
|
|
'''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
|
|
|
|
# multiply_factor = round(keyframe.co[0])/keyframe.co[0]
|
|
new_value = keyframe.co.lerp(handle, factor)
|
|
new_handles.append(new_value[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:
|
|
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 '''
|
|
channelbag = Tools.get_channelbag(obj, obj.animation_data.action)
|
|
for group in channelbag.groups:
|
|
for channel in group.channels:
|
|
if path == channel.data_path:
|
|
return group
|
|
return None
|
|
|
|
def get_frame_keys(obj, fcurves, path, array_len, frames):
|
|
'''Creating a dict with the keyframe points from all the location channels to be used for the handles'''
|
|
frame_keys = dict()
|
|
#iterate over the location arrays
|
|
key_fcurves = []
|
|
for i in range(array_len):
|
|
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]
|
|
# Subframes will be disabled because they don't exist inside the rounded keyframes
|
|
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] * array_len})
|
|
frame_keys[frame][i] = keyframe_point
|
|
key_fcurves.append(fcu)
|
|
|
|
return frame_keys, key_fcurves
|
|
|
|
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(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)
|
|
|
|
influence = strip.influence if strip.use_animated_influence else 1
|
|
|
|
value_add[i] = handles_add_to_value(value_add[i], fcu, frame, 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(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, context):
|
|
'''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, array_len = get_fcu_path(context, self.posemode, obj, obj_bonename[1])
|
|
fcurves = Tools.get_fcurves(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, array_len, frames)
|
|
frame_keys, key_fcurves = get_frame_keys(obj, fcurves, path, array_len, 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)
|
|
handles_left_frame, handle_left_values = get_avg_handles('handle_left', keys, handles_left)
|
|
handles_right_frame = add_frame_offset(obj, handles_right_frame, add = False)
|
|
handles_left_frame = add_frame_offset(obj, handles_left_frame, add = False)
|
|
|
|
if self.channels == 'HEADS':
|
|
if None in keys:
|
|
#if a keyframe is missing then get the array from the matrix basis
|
|
handle_right_values = [bone.matrix_basis.translation[i] if key is None else handle_right_values[i] for i, key in enumerate(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)
|
|
|
|
get_handle_type(self, obj_bonename, frame, keys)
|
|
|
|
if self.channels == 'HEADS':
|
|
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 self.camera_space:
|
|
cam = context.scene.camera
|
|
bone_right_matrix = bone_right_matrix @ cam.matrix_world.inverted()
|
|
bone_right_matrix_basis = bone_right_matrix_basis @ cam.matrix_world.inverted()
|
|
handle_right_coords = bone_right_matrix @ bone_right_matrix_basis.inverted() @ Vector(handle_right_values)
|
|
else:
|
|
# In case of rotation motion path, get only the coordinates of the tail
|
|
# Can eventually do this without using frame_set, just recalculating the subframe
|
|
handle_r_i = int(handles_right_frame) - self.avz_frame_start[obj_bonename]
|
|
right_coord = Vector(self.bones_points[obj_bonename][handle_r_i])
|
|
next_coord = Vector(self.bones_points[obj_bonename][handle_r_i+1])
|
|
handle_right_coords = right_coord.lerp(next_coord, handles_right_frame % 1)
|
|
# handle_right_coords = bone.tail.copy()
|
|
|
|
if self.channels == 'HEADS':
|
|
#Getting the coordinates for the Left handles
|
|
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()
|
|
handle_left_coords = bone_left_matrix @ bone_left_matrix_basis.inverted() @ Vector(handle_left_values)
|
|
else:
|
|
# In case of rotation motion path, get only the coordinates of the tail
|
|
handle_l_i = int(handles_left_frame) - self.avz_frame_start[obj_bonename]
|
|
left_coord = Vector(self.bones_points[obj_bonename][handle_l_i])
|
|
prev_coord = Vector(self.bones_points[obj_bonename][handle_l_i+1])
|
|
handle_left_coords = left_coord.lerp(prev_coord, handles_left_frame % 1)
|
|
# handle_left_coords = bone.tail.copy()
|
|
|
|
if self.posemode:
|
|
handle_right_coords = obj.matrix_world @ handle_right_coords
|
|
handle_left_coords = obj.matrix_world @ handle_left_coords
|
|
|
|
# Add Camera or Cursor offsets
|
|
if self.channels == 'HEADS':
|
|
if self.camera_space:
|
|
i = round(frame - self.avz_frame_start[obj_bonename])
|
|
scene.frame_set(int(frame), subframe = frame % 1)
|
|
# Getting the offset from the actual world space position to remove from the handles
|
|
cam_space_offset = bone.matrix.translation - Vector(self.bones_points[obj_bonename][i])
|
|
handle_right_coords -= cam_space_offset
|
|
handle_left_coords -= cam_space_offset
|
|
|
|
elif self.cursor_offset:
|
|
coords_cursor_offset(bone, handle_right_coords)
|
|
coords_cursor_offset(bone, 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, context)
|
|
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, context)
|
|
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:
|
|
return False
|
|
|
|
self.initial_keyframe_coords = copy.deepcopy(self.bones_keyframes_coords)
|
|
|
|
if len(frames):
|
|
update_mp_points(self, context, frames, None, None)
|
|
else:
|
|
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, context)
|
|
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):
|
|
'''Updating the keyframes coords, this comes always after updating the mp points'''
|
|
self.bones_keyframes_coords = dict()
|
|
|
|
for obj_bonename, points in self.bones_points.items():
|
|
keyframes = self.mp_bones_keys[obj_bonename]
|
|
start = self.avz_frame_start[obj_bonename]
|
|
keyframes_coords = dict()
|
|
|
|
# Generate frame numbers as a NumPy array
|
|
frames = np.arange(start, start + len(points))
|
|
|
|
# Boolean mask for frames that exist in keyframes
|
|
mask = np.isin(frames, keyframes)
|
|
|
|
# Filter frames and corresponding coords
|
|
valid_frames = frames[mask]
|
|
# valid_coords = points[mask]
|
|
|
|
# Vectorized dictionary creation using zip, converting to int values to be used later with Json
|
|
# keyframes_coords = {int(f): np.copy(coords) for f, coords in zip(valid_frames, valid_coords)}
|
|
keyframes_coords = {int(f): points[i] for f, i in zip(valid_frames, valid_frames - start)}
|
|
|
|
self.bones_keyframes_coords[obj_bonename] = keyframes_coords
|
|
|
|
def check_removed_items(self, context, emp):
|
|
''' Checking if the item was removed'''
|
|
if not hasattr(self, 'items'):
|
|
return False
|
|
|
|
for item in self.items:
|
|
obj = item.id_data
|
|
if obj not in context.view_layer.objects.values():
|
|
emp.motion_path = False
|
|
quit_mp(self, context)
|
|
return True
|
|
|
|
return False
|
|
|
|
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, context):
|
|
'''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()
|
|
|
|
key_props = ['co', 'handle_left','handle_right']# 'handle_left_type', 'handle_right_type' , 'interpolation'
|
|
|
|
for item in current_selected_items:
|
|
|
|
if item in self.items and context.scene.emp.channels == 'HEADS':
|
|
# Get only the relevant transform for location motion path item
|
|
transforms = [get_transform_channel(self.posemode, context, item)]
|
|
else:
|
|
# Replace the rotation path with the actual rotation of the selection
|
|
rotation = 'rotation_euler' if len(item.rotation_mode) == 3 else 'rotation_' + item.rotation_mode.lower()
|
|
transforms = ['location', rotation, 'scale']
|
|
|
|
#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(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:
|
|
print('object 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,
|
|
self.batch_line, self.batch_points, self.batch_selected, self.batch_keyframes]
|
|
|
|
# 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 ProportionalEditor:
|
|
# Class-level constants for interpolation types
|
|
LINEAR = "linear"
|
|
SMOOTH = "smooth"
|
|
SPHERE = "sphere"
|
|
ROOT = "root"
|
|
INVERSE_SQUARE = "inverse_square"
|
|
SHARP = "sharp"
|
|
CONSTANT = "constant"
|
|
RANDOM = "random"
|
|
|
|
def __init__(self):
|
|
# Dictionary mapping interpolation types to their functions
|
|
self.interpolation_functions = {
|
|
'SMOOTH': self._smooth,
|
|
'SPHERE': self._sphere,
|
|
'ROOT': self._root,
|
|
'INVERSE_SQUARE': self._inverse_square,
|
|
'SHARP': self._sharp,
|
|
'LINEAR': self._linear,
|
|
'CONSTANT': self._constant,
|
|
'RANDOM': self._random,
|
|
}
|
|
|
|
# Interpolation functions as methods
|
|
def _linear(self, t):
|
|
return 1 - t
|
|
|
|
def _smooth(self, t):
|
|
# Smooth step function: 3t² - 2t³
|
|
return 1-(3 * t * t - 2 * t * t * t)
|
|
|
|
def _sphere(self, t):
|
|
# Spherical falloff: sqrt(1 - (1-t)²)
|
|
return math.sqrt(1- t * t)
|
|
|
|
def _root(self, t):
|
|
# Square root falloff
|
|
return 1- math.sqrt(t)
|
|
|
|
def _inverse_square(self, t):
|
|
# Inverse square falloff: 1 - t²
|
|
return 1 - t * t
|
|
|
|
def _sharp(self, t):
|
|
# Sharp falloff: t⁴
|
|
return (1 - t) * (1 - t) * (1 - t) * (1 - t)
|
|
|
|
def _constant(self, t):
|
|
# Constant falloff (step function)
|
|
return 1.0 if t < 1.0 else 0.0
|
|
|
|
def _random(self, t):
|
|
# Random falloff
|
|
return np.random.random() * (1 - t)
|
|
|
|
def apply_interpolation(self, normalized_distance, interpolation_type):
|
|
"""
|
|
Apply interpolation curve to normalized distance (0-1 range)
|
|
|
|
Args:
|
|
normalized_distance: float between 0-1, where 0 is at center, 1 is at edge
|
|
interpolation_type: string matching class constants
|
|
|
|
Returns:
|
|
float: transformed distance value
|
|
"""
|
|
# Clamp to 0-1 range
|
|
t = max(0, min(1, normalized_distance))
|
|
|
|
# Get the interpolation function from dictionary
|
|
interpolation_func = self.interpolation_functions.get(interpolation_type) # # Default fallback
|
|
|
|
return interpolation_func(t)
|
|
|
|
|
|
class Markers_ManualRange(bpy.types.Operator):
|
|
"""Create Markers for an interactive Motion Path Manual frame range"""
|
|
bl_idname = "anim.emp_markers_range"
|
|
bl_label = "Emp_Markers_Framerange"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
frame_start: bpy.props.IntProperty(name = 'Frame Start', description="Storing the frame start to check if the property or marker were changed")
|
|
frame_end: bpy.props.IntProperty(name = 'Frame End', description="Storing the frame end to check if the property or marker were changed")
|
|
|
|
def execute(self, context):
|
|
context.scene.emp.marker_frame_range = False
|
|
context.scene.timeline_markers.remove(self.frame_start_marker)
|
|
context.scene.timeline_markers.remove(self.frame_end_marker)
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
#If the modal is already running, then don't run it the second time
|
|
scene = context.scene
|
|
emp = scene.emp
|
|
if emp.marker_frame_range:
|
|
emp.marker_frame_range = False
|
|
return {'CANCELLED'}
|
|
|
|
emp.marker_frame_range = True
|
|
|
|
self.frame_start = emp.frame_start
|
|
self.frame_end = emp.frame_end
|
|
#init_frame_range_markers(self, scene)
|
|
Tools.init_frame_start_marker(self, scene, name = 'MotionPath Start')
|
|
Tools.init_frame_end_marker(self, scene, name = 'MotionPath End')
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
scene = context.scene
|
|
|
|
if Tools.check_removed_markers(self, scene):
|
|
return {'PASS_THROUGH'}
|
|
|
|
#If the modal was running again or changed then exit
|
|
if not scene.emp.marker_frame_range:
|
|
self.execute(context)
|
|
return {'FINISHED'}
|
|
|
|
if Tools.update_frame_range_markers(self, scene.emp, 'frame_start', 'frame_end'):
|
|
return {'RUNNING_MODAL'}
|
|
|
|
if event.type in {'ESC'}: # Cancel
|
|
self.execute(context)
|
|
return {'FINISHED'}
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
|
|
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
|
|
self.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)
|
|
|
|
def disbale_non_select_circle(self, context):
|
|
if self.smooth:
|
|
self['smooth'] = False
|
|
if self.prop_edit:
|
|
self['prop_edit'] = False
|
|
|
|
def disbale_non_prop_edit(self, context):
|
|
if self.smooth:
|
|
self['smooth'] = False
|
|
if self.select_circle:
|
|
self['select_circle'] = False
|
|
|
|
def disbale_non_smooth(self, context):
|
|
if self.prop_edit:
|
|
self['prop_edit'] = False
|
|
if self.select_circle:
|
|
self['select_circle'] = False
|
|
|
|
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'})
|
|
refresh: bpy.props.BoolProperty(name = "Refresh Motion Path", description = "Refresh the motion paths with hotkey ctrl", 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'})
|
|
|
|
radius: bpy.props.IntProperty(name = "Radius", description = "Radius for proportional editing", min = 1, max = 800, default = 30)
|
|
smooth: bpy.props.BoolProperty(name = "Smooth Keyframes", description = "Smoothing keyframes on the motion path", default = False, update=disbale_non_smooth)
|
|
smooth_strength: bpy.props.FloatProperty(name = "Smooth Strength", description = "How strong smoothing will apply", min = 0, max = 1, default = 0.05)
|
|
|
|
prop_edit: bpy.props.BoolProperty(name = "Proportional Editing", description = "Move multiple keyframes with proportional editing", default = False, update=disbale_non_prop_edit )
|
|
prop_edit_falloff: bpy.props.EnumProperty(name = 'Propotional Editing Falloff', description="Set the falloff interpolation for the motion path proportional editing", default = 'SMOOTH',
|
|
items = [('SMOOTH','smooth', 'smooth', 'SMOOTHCURVE', 0),
|
|
('SPHERE', 'sphere', 'sphere', 'SPHERECURVE', 1),
|
|
('ROOT','root', 'root', 'ROOTCURVE', 2),
|
|
('INVERSE_SQUARE','inverse_square', 'inverse_square', 'INVERSESQUARECURVE', 3),
|
|
('SHARP','sharp', 'sharp', 'SHARPCURVE', 4),
|
|
('LINEAR', 'linear', 'linear', 'IPO_LINEAR', 5),
|
|
('CONSTANT','constant', 'constant', 'IPO_CONSTANT', 6),
|
|
('RANDOM','random', 'random', 'IPO_ELASTIC', 7)])
|
|
select_circle: bpy.props.BoolProperty(name = "Selection Brush", description = "Select multiple Keyframes using the selection brush", default = False, update = disbale_non_select_circle)
|
|
|
|
camera_space: bpy.props.BoolProperty(name = "Camera Space", description = "Baking the motion path into the active camera space", default = False)
|
|
cursor_offset: bpy.props.BoolProperty(name = "Cursor Offset", description = "Offseting the Motion Path to the cursor position", default = False)
|
|
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)])
|
|
|
|
channels: bpy.props.EnumProperty(name = 'Channel', description="Type of Frame Range", default = 'HEADS',
|
|
items = [('HEADS', 'Location','Create Motion Path on location channels','OBJECT_ORIGIN', 0),
|
|
('TAILS', 'Rotation','Create Motion Path on rotation channels','TRACKING', 1)])
|
|
|
|
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)
|
|
marker_frame_range: bpy.props.BoolProperty(name = "Marker Frame Range", description = "Flag when marker frame range turned on", default = False)
|
|
|
|
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','Velocity', 'Velocity 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, Markers_ManualRange)
|
|
|
|
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 |