2998 lines
139 KiB
Python
2998 lines
139 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
|
|
|
|
|
|
def coords_2d_update(self, context):
|
|
'''update the 2d coordinates from the 3d coordinates, when the screen moves'''
|
|
self.coords2d_ranges = []
|
|
self.coords2d_bones_frames = dict()
|
|
self.coords2d_handles_r = dict()
|
|
self.coords2d_handles_l = dict()
|
|
#when pressing ctrl, use all points to hover, for adding new keyframe points
|
|
if self.ctrl:
|
|
self.coords2d_bones_frames = coords_2d_add(self, context, self.bones_allframes_coords)
|
|
# bones_frames_coords = self.bones_allframes_coords
|
|
else:
|
|
#otherwise get only the keyframes
|
|
self.coords2d_bones_frames = coords_2d_add(self, context, self.bones_keyframes_coords)
|
|
#and coordinates of the handles
|
|
if self.bones_handles_right:
|
|
self.coords2d_handles_r = coords_2d_add(self, context, self.bones_handles_right)
|
|
self.coords2d_handles_l = coords_2d_add(self, context, self.bones_handles_left)
|
|
|
|
|
|
def coords_2d_add(self, context, bones_frames_coords):
|
|
'''Get all the coordinates withing the range value from keys or handles'''
|
|
range_value = self.range_value
|
|
coords2d_bones_frames = dict()
|
|
for obj_bonename, keyframes_coords in bones_frames_coords.items():
|
|
for frame, coord_3d in keyframes_coords.items():
|
|
coord_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, coord_3d)
|
|
if not coord_2d:
|
|
continue
|
|
coord_2d = tuple(coord_2d)
|
|
coord_x = int(coord_2d[0])
|
|
coord_y = int(coord_2d[1])
|
|
#continue if the coordinates are out of the screnn
|
|
if coord_x < 0 or coord_y < 0:
|
|
continue
|
|
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 {'PASS_THROUGH'}
|
|
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()}
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
return {'PASS_THROUGH'}
|
|
coords_2d, bone_frame = get_keyframe_coord2d(self)
|
|
|
|
if coords_2d in self.coords2d_bones_frames:
|
|
self.hover_bone_frame = {key : bone_frame if key == 'keyframe' else None for key in self.hover_bone_frame.keys()}
|
|
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()}
|
|
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()}
|
|
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
|
|
def filter_axis_distance(self, points, initial_coords):
|
|
'''Filtering the X Y Z from the location while scaling or moving the keyframes'''
|
|
if not self.scale and not self.press and not self.rotate:
|
|
return points
|
|
if self.filter_axis == '':
|
|
('cancel filtering')
|
|
return points
|
|
new_points = initial_coords.copy()
|
|
axis_array = {'X' : 0, 'Y' : 1, 'Z' : 2}
|
|
filter_axis = self.text.split(' along world ')[1]
|
|
for axis in filter_axis:
|
|
i = axis_array[axis]
|
|
new_points[i] = points[i]
|
|
return new_points
|
|
|
|
def filter_axis_keyframes(self, event):
|
|
|
|
if not self.press and not self.scale and not self.rotate:
|
|
return False
|
|
# if self.filter_axis is not None:
|
|
# return False
|
|
if event.type not in {'X', 'Y', 'Z'}:
|
|
return False
|
|
if event.value != 'PRESS':
|
|
return False
|
|
if not self.bones_selected_keyframes:
|
|
return False
|
|
if not any(self.hover_bone_frame.values()):
|
|
return False
|
|
|
|
axis = 'XYZ'.replace(event.type, '') if self.shift else event.type
|
|
filter_axis = ' along world ' + axis
|
|
|
|
#Remove filter axis when double click on the axis
|
|
self.filter_axis = '' if self.filter_axis == filter_axis else filter_axis
|
|
if ' along world ' in self.text:
|
|
self.text = self.text.split(' along world ')[0]
|
|
|
|
self.text = self.text + self.filter_axis
|
|
|
|
def move_rotate_scale_keyframes(self, event):
|
|
|
|
esc = True if not any([self.press, self.rotate, self.scale]) and hasattr(self, 'draw_handle') else False
|
|
# print('escape ', esc)
|
|
Tools.update_notification(self, bpy.context, event, esc, fade_out = False)
|
|
|
|
if event.type not in {'G','R', 'S'}:
|
|
return False
|
|
# if self.scale or self.rotate:
|
|
# return False
|
|
if event.value != 'PRESS':
|
|
return False
|
|
if not self.bones_selected_keyframes:
|
|
return False
|
|
if not any(self.hover_bone_frame.values()):
|
|
return False
|
|
|
|
obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self)
|
|
#if it's not in the selected keyframes then skip
|
|
if obj_bonename in selection:
|
|
if frame not in selection[obj_bonename]:
|
|
return False
|
|
else:
|
|
return False
|
|
|
|
#scaling and rotating need minimum of 2 selected keyframes
|
|
if len(self.bones_selected_keyframes[obj_bonename]) < 2 and event.type in {'R', 'S'}:
|
|
return False
|
|
|
|
initialize_g_r_s(self, event.type)
|
|
|
|
if key == 'keyframe':
|
|
self.initial_keyframe_coords = copy.deepcopy(self.bones_keyframes_coords)
|
|
else:
|
|
self.initial_handles_right = copy.deepcopy(self.bones_handles_right)
|
|
self.initial_handles_left = copy.deepcopy(self.bones_handles_left)
|
|
|
|
self.initial_coord = Vector(coord_3d)
|
|
self.prev_event_value = None
|
|
|
|
def initialize_g_r_s(self, transform):
|
|
'''set the attribute for the operator and reset the others'''
|
|
|
|
op = {'G' : 'press', 'R' : 'rotate', 'S' : 'scale'}
|
|
text_dict = {'G' : 'Move Keyframes ', 'R' : 'Rotate Keyframes ', 'S' : 'Scale Keyframes '}
|
|
|
|
if getattr(self, op[transform]):
|
|
return
|
|
setattr(self, op[transform], True)
|
|
|
|
#list of the values of the other operators to check if any of them is True
|
|
other_ops = {k : getattr(self, v) for k, v in op.items() if k != transform}
|
|
|
|
if any(other_ops.values()):
|
|
self.text = text_dict[transform]
|
|
#Previous operators get False
|
|
for k, v in other_ops.items():
|
|
setattr(self, op[k], False)
|
|
self.filter_axis = ''
|
|
self.bones_keyframes_coords = self.initial_keyframe_coords
|
|
else:
|
|
Tools.notification_invoke(self, bpy.context, text = text_dict[transform], size = 15)
|
|
|
|
def scale_points(points, pivot, scale_factor):
|
|
# for i, point in enumerate(points):
|
|
direction = points - pivot
|
|
scaled_points = pivot + direction * scale_factor
|
|
|
|
return scaled_points
|
|
|
|
def project_onto_view_plane(vector, view_matrix):
|
|
# Get the view direction from the view matrix
|
|
view_direction = view_matrix.to_quaternion() @ Vector((0.0, 0.0, -1.0))
|
|
# Project the vector onto the plane perpendicular to the view direction
|
|
projection = vector - vector.project(view_direction)
|
|
return projection
|
|
|
|
def get_rotation_2d(context, initial_coord, new_coord, pivot_2d):
|
|
'''rotation the points in 2d'''
|
|
new_coord_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, new_coord)
|
|
initial_coord_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, initial_coord)
|
|
|
|
vector1_2d = initial_coord_2d - pivot_2d
|
|
vector2_2d = new_coord_2d - pivot_2d
|
|
|
|
angle = vector1_2d.angle(vector2_2d)
|
|
|
|
#check the direction it should rotate
|
|
cross_product = vector1_2d.x * vector2_2d.y - vector1_2d.y * vector2_2d.x
|
|
if cross_product > 0:
|
|
angle *= -1
|
|
|
|
rotation_matrix = Matrix.Rotation(angle, 2, 'Z')
|
|
|
|
return rotation_matrix, angle
|
|
|
|
def get_location_path(posemode, obj, bonename):
|
|
'''get the datapath of the location, check if it's a bone or object'''
|
|
if posemode:
|
|
posebone = obj.pose.bones[bonename]
|
|
path = posebone.path_from_id() + '.location'
|
|
else:
|
|
path = 'location'
|
|
|
|
return path
|
|
|
|
def get_keyframe_locations(self, keyframe_coords):
|
|
''' get a list with the average of all the keyframe world locations'''
|
|
|
|
locations = []
|
|
for obj_bonename, frames in self.bones_selected_keyframes.items():
|
|
for frame in frames:
|
|
locations.append(keyframe_coords[obj_bonename][frame])
|
|
|
|
return locations
|
|
|
|
def zoom_keyframes(self, context, event):
|
|
if event.type != 'NUMPAD_PERIOD':
|
|
return False
|
|
if (int(self.mouse_x), int(self.mouse_y)) not in self.coords2d_ranges:
|
|
return False
|
|
|
|
locations = get_keyframe_locations(self, self.bones_keyframes_coords)
|
|
avg_loc = sum(locations) / len(locations)
|
|
context.region_data.view_location = avg_loc
|
|
max_loc = max([Vector(loc) for loc in locations])
|
|
min_loc = min([Vector(loc) for loc in locations])
|
|
context.region_data.view_distance = (max_loc - min_loc).length + (context.scene.animtoolbox.mp_keyframe_scale **2)
|
|
if context.region_data.view_distance < 1:
|
|
context.region_data.view_distance = 1
|
|
context.region_data.view_perspective = 'PERSP'
|
|
return True
|
|
|
|
def unpack_hover_frame(self):
|
|
for key, value in self.hover_bone_frame.items():
|
|
if value is None:
|
|
continue
|
|
obj_bonename, frame = next(iter(value.items()))
|
|
if key == 'keyframe':
|
|
selection = self.bones_selected_keyframes
|
|
coord_3d = self.bones_keyframes_coords[obj_bonename][frame]
|
|
elif key == 'handle_r':
|
|
selection = self.bones_selected_handles_r
|
|
coord_3d = self.bones_handles_right[obj_bonename][frame]
|
|
elif key == 'handle_l':
|
|
selection = self.bones_selected_handles_l
|
|
coord_3d = self.bones_handles_left[obj_bonename][frame]
|
|
break
|
|
return obj_bonename, key, frame, selection, coord_3d
|
|
|
|
def left_mouse_press(self, context):
|
|
'''behavior when pressing on the left mouse to select or start'''
|
|
|
|
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)
|
|
|
|
if obj_bonename in selection:
|
|
if frame in selection[obj_bonename]:
|
|
#store the frame that is being moved
|
|
# self.moving_frame = {obj_bonename : (key, frame)}
|
|
self.initial_coord = Vector(coord_3d)
|
|
|
|
#if holding SHIFT then DESELECT the frame, remove selection and handles
|
|
if self.shift:
|
|
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]
|
|
|
|
coords_2d_update(self, context)
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
# bpy.ops.ed.undo_push(message = 'Deselect motion path keyframe')
|
|
return True
|
|
else:
|
|
#start dragging the keyframe
|
|
initialize_g_r_s(self, 'G')
|
|
if key == 'keyframe':
|
|
self.initial_keyframe_coords = copy.deepcopy(self.bones_keyframes_coords)
|
|
else:
|
|
#create a copy of the handles in case operation is cancelled
|
|
self.initial_handles_right = copy.deepcopy(self.bones_handles_right)
|
|
self.initial_handles_left = copy.deepcopy(self.bones_handles_left)
|
|
# self.press = True
|
|
return True
|
|
|
|
if self.shift:
|
|
#ADD to selection using shift
|
|
if obj_bonename in selection:
|
|
selection[obj_bonename].append(frame)
|
|
else:
|
|
selection.update({obj_bonename : [frame]})
|
|
get_handles(self)
|
|
else:
|
|
#SELECT the keyframe, and deselect all others
|
|
selection.clear()
|
|
handles_selection_clear(self)
|
|
if key != 'keyframe':
|
|
self.bones_selected_keyframes = {obj_bonename : [frame]}
|
|
|
|
selection.update({obj_bonename : [frame]})
|
|
get_handles(self)
|
|
|
|
coords_2d_update(self, context)
|
|
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
|
|
context.scene.animtoolbox.selected_keyframes = serialize_dict(self.bones_selected_keyframes)
|
|
bpy.ops.ed.undo_push(message = 'Select Motion Path Keyframe')
|
|
|
|
return True
|
|
|
|
def edit_motion_path_update(self, context, event):
|
|
'''update mouse interaction with editable motion paths'''
|
|
scene = context.scene
|
|
|
|
if not self.coords2d_ranges:
|
|
coords_2d_update(self, context)
|
|
|
|
#if view angle is changed then update the 2d coords of the keyframess
|
|
if self.prev_view_matrix != context.region_data.view_matrix or self.prev_window_matrix != context.region_data.window_matrix:
|
|
self.prev_view_matrix = context.region_data.view_matrix.copy()
|
|
self.prev_window_matrix = context.region_data.window_matrix.copy()
|
|
coords_2d_update(self, context)
|
|
|
|
mouse_hover_keyframe(self, event)
|
|
|
|
# g_move_keyframes(self, event)
|
|
move_rotate_scale_keyframes(self, event)
|
|
filter_axis_keyframes(self, event)
|
|
|
|
if event.type == self.select_mouse: #'LEFTMOUSE'
|
|
frame_current = scene.frame_current_final
|
|
|
|
#add or remove keyframes using Ctrl hotkey
|
|
if event.value == 'RELEASE' and self.ctrl:
|
|
if not self.hover_bone_frame['keyframe']:
|
|
return True
|
|
|
|
obj_bonename, frame = next(iter(self.hover_bone_frame['keyframe'].items()))
|
|
|
|
key_frames = self.mp_bones_keys[obj_bonename]
|
|
obj = context.view_layer.objects[obj_bonename[0]]
|
|
# posebone = obj.pose.bones[obj_bonename[1]]
|
|
path = get_location_path(self.posemode, obj, obj_bonename[1])
|
|
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
|
|
#if there is a keyframe then remove it
|
|
if frame in key_frames:
|
|
|
|
#removing keyframes
|
|
for i in range(3):
|
|
fcu = fcurves.find(data_path = path, index = i)
|
|
if fcu is None:
|
|
continue
|
|
for keyframe in fcu.keyframe_points:
|
|
if round(add_frame_offset(obj, keyframe.co[0])) == frame:
|
|
fcu.keyframe_points.remove(keyframe)
|
|
|
|
#deselect if selected
|
|
if obj_bonename in self.bones_selected_keyframes:
|
|
if frame in self.bones_selected_keyframes[obj_bonename]:
|
|
self.bones_selected_keyframes[obj_bonename].remove(frame)
|
|
handles_frame_remove(self, obj_bonename, frame)
|
|
self.mp_bones_keys[obj_bonename].remove(frame)
|
|
|
|
else: #if there is no keyframe then add a new one
|
|
if frame != frame_current:
|
|
scene.frame_set(int(frame), subframe = frame%1)
|
|
#Offset is already applied to the frame
|
|
obj.keyframe_insert(data_path = path, frame = frame)
|
|
self.mp_bones_keys[obj_bonename].append(frame)
|
|
self.mp_bones_keys[obj_bonename].sort()
|
|
|
|
#reset the motion path
|
|
update_mp_points(self, context, [frame], [obj_bonename])
|
|
# get_mp_points(self, context)
|
|
coords_2d_update(self, context)
|
|
self.current_bones_keys = get_current_bones_keys(self)
|
|
|
|
scene.frame_set(int(frame_current), subframe = frame_current % 1)
|
|
|
|
#update selected keyframes property serialization for undo purpose
|
|
context.scene.animtoolbox.selected_keyframes = serialize_dict(self.bones_selected_keyframes)
|
|
bpy.ops.ed.undo_push(message = 'Add / Remove Keyframe')
|
|
|
|
return True
|
|
|
|
if event.value == 'PRESS':
|
|
if self.prev_event_value != 'NOTHING':
|
|
self.prev_event_value = event.value
|
|
if left_mouse_press(self, context):
|
|
return True
|
|
|
|
if event.value == 'RELEASE' and (self.press or self.scale or self.rotate):
|
|
obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self)
|
|
|
|
#if it was just a short click then just select the keyframe in case of multiple keyframes are selected
|
|
if self.prev_event_value == 'PRESS':
|
|
context.scene.animtoolbox.selected_keyframes = serialize_dict(self.bones_selected_keyframes)
|
|
bpy.ops.ed.undo_push(message = 'Select Motion Path Keyframe')
|
|
|
|
handles_selection_clear(self)
|
|
self.bones_selected_keyframes = {obj_bonename : [frame]}
|
|
if key == 'handle_l':
|
|
self.bones_selected_handles_l = {obj_bonename : [frame]}
|
|
elif key == 'handle_r':
|
|
self.bones_selected_handles_r = {obj_bonename : [frame]}
|
|
|
|
self.press = False
|
|
get_handles(self)
|
|
coords_2d_update(self, context)
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
return True
|
|
|
|
###Apply the keyframe movement###
|
|
#get the new coordinates and distance from the selected frame
|
|
#Using obj_bonename for hovered object to get the distance, and the selected_obj_bonename
|
|
# for the rest of the selected bones
|
|
|
|
for selected_obj_bonename, frames in self.bones_selected_keyframes.items():
|
|
objname, bonename = selected_obj_bonename
|
|
obj = context.view_layer.objects[objname]
|
|
#if we are using an object then treat the object as a bone when getting the distance
|
|
posebone = obj.pose.bones[bonename] if self.posemode else obj
|
|
|
|
#get the distance from the initial coords
|
|
if self.press:
|
|
#get only a single distance when only moving/grabing the keyframes
|
|
frame_distance = get_distance(key, self, context, posebone, obj_bonename, frame, self.matrix_diff[selected_obj_bonename], coord_3d)
|
|
elif self.scale or self.rotate:
|
|
# get the distance on each frame separatly
|
|
frame_distance = dict()
|
|
for frame in frames:
|
|
distance = get_distance(key, self, context, posebone, obj_bonename, frame, self.matrix_diff[selected_obj_bonename], coord_3d)
|
|
frame_distance.update({frame : distance})
|
|
#apply the coordinates and distance to all the selected keyframes and handles
|
|
mp_update_keyframes(self, context, selected_obj_bonename, frame_distance, key) #
|
|
|
|
if scene.frame_current_final != frame_current:
|
|
context.scene.frame_set(int(frame_current), subframe = frame_current % 1)
|
|
|
|
update_frames = [frame for frames in self.bones_selected_keyframes.values() for frame in frames]
|
|
update_mp_points(self, context, update_frames, self.bones_selected_keyframes.keys(), key, coord_3d)
|
|
# get_mp_points(self, context)
|
|
self.coords2d_bones_frames = None
|
|
coords_2d_update(self, context)
|
|
self.hover_bone_frame = {'keyframe': None, 'handle_r' : None, 'handle_l' : None}
|
|
# self.moving_frame = dict()
|
|
self.initial_coord = None
|
|
self.initial_keyframe_coords = None
|
|
self.prev_event_value = event.value
|
|
self.press = False
|
|
self.scale = False
|
|
self.rotate = False
|
|
self.filter_axis = ''
|
|
self.current_bones_keys = get_current_bones_keys(self)
|
|
|
|
context.scene.animtoolbox.selected_keyframes = serialize_dict(self.bones_selected_keyframes)
|
|
bpy.ops.ed.undo_push(message = 'Move Motion Path Keyframe')
|
|
return True
|
|
|
|
elif event.type == 'MOUSEMOVE' and (self.press or self.scale or self.rotate):
|
|
#updating only the moving keyframe
|
|
obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self)
|
|
|
|
#get the new coordinates and distance from the selected moving keyframe
|
|
loc_coord = Vector(bpy_extras.view3d_utils.region_2d_to_location_3d(context.region, context.region_data, (self.mouse_x, self.mouse_y), coord_3d))
|
|
distance = loc_coord - Vector(coord_3d)
|
|
|
|
# context.scene.cursor.location = loc_coord
|
|
if not self.press:
|
|
locations = get_keyframe_locations(self, self.initial_keyframe_coords)
|
|
pivot = Vector(sum(locations) / len(locations))
|
|
|
|
if self.press:
|
|
#adding text with all the detail
|
|
axis_array = {0 : 'X', 1 : 'Y', 2 : 'Z'}
|
|
if self.filter_axis == '':
|
|
move_distance = loc_coord - self.initial_coord
|
|
else:
|
|
move_distance = (value if axis_array[i] in self.filter_axis else 0.0 for i, value in enumerate (loc_coord - self.initial_coord))
|
|
distance_text = ' '.join(axis_array[i] + ': ' + str(round(value, 2)) for i, value in enumerate(move_distance))
|
|
self.text = 'Move Keyframes ' + distance_text + self.filter_axis
|
|
|
|
elif self.rotate:
|
|
pivot_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, pivot)
|
|
rotation_matrix, angle = get_rotation_2d(context, self.initial_coord, loc_coord, pivot_2d)
|
|
self.text = 'Rotate Keyframes ' + str(round(math.degrees(angle), 2)) + self.filter_axis
|
|
|
|
elif self.scale:
|
|
try:
|
|
scale_factor = (loc_coord - pivot).length / (self.initial_coord - pivot).length
|
|
self.text = 'Scale Keyframes ' + str(round(scale_factor, 2)) + self.filter_axis
|
|
except ZeroDivisionError:
|
|
return True
|
|
# Check the direction using the dot product
|
|
dot_product = (loc_coord - pivot).dot(self.initial_coord - pivot)
|
|
dot = -1 if dot_product < 0 else 1
|
|
|
|
#add the distance to all the selected keyframes or the selected handles
|
|
for obj_bonename in selection.keys():
|
|
if key == 'keyframe':
|
|
keyframes_coords = self.bones_keyframes_coords[obj_bonename]
|
|
#get all the points from the curve of this bone
|
|
points = list(self.bones_points[obj_bonename])
|
|
frames_distance = dict()
|
|
|
|
for frame, coord_3d in keyframes_coords.items():
|
|
if frame not in selection[obj_bonename]:
|
|
continue
|
|
initial_coords = Vector(self.initial_keyframe_coords[obj_bonename][frame])
|
|
#get the index of the point based on the frame number
|
|
i = (round(frame - self.avz_frame_start[obj_bonename])) *3
|
|
coords = Vector(points[i : i+3])
|
|
if Vector(coord_3d) != Vector(coords):
|
|
continue
|
|
|
|
if self.press:
|
|
new_coord = initial_coords + (Vector(loc_coord) - self.initial_coord)
|
|
elif self.scale:
|
|
#scale only the initial coords and add it as a distance
|
|
new_coord = scale_points(initial_coords, pivot, scale_factor * dot)
|
|
|
|
elif self.rotate:
|
|
initial_coord_2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, initial_coords)
|
|
rotated_vector = (initial_coord_2d - pivot_2d) @ rotation_matrix + pivot_2d
|
|
new_coord = Vector(bpy_extras.view3d_utils.region_2d_to_location_3d(context.region, context.region_data, rotated_vector, coord_3d))
|
|
|
|
new_coord = filter_axis_distance(self, new_coord, initial_coords)
|
|
#get the distance of the handles from the keyframe last position
|
|
distance = Vector(new_coord - Vector(self.bones_points[obj_bonename][i:i+3]))
|
|
frames_distance.update({frame : distance})
|
|
|
|
#apply the keyframe scaled position
|
|
self.bones_points[obj_bonename][i:i+3] = new_coord
|
|
|
|
if obj_bonename in self.bones_handles_right:
|
|
#get the distance from the keyframes
|
|
self.bones_handles_right[obj_bonename][frame] += distance
|
|
self.bones_handles_left[obj_bonename][frame] += distance
|
|
|
|
add_distance_to_points(self, obj_bonename, frames_distance)
|
|
|
|
else:
|
|
handle_coords_r = self.bones_handles_right[obj_bonename]
|
|
handle_coords_l = self.bones_handles_left[obj_bonename]
|
|
|
|
#Move the right handle
|
|
for frame, coord_3d in handle_coords_r.items():
|
|
if obj_bonename not in self.bones_selected_handles_r:
|
|
continue
|
|
if frame not in self.bones_selected_handles_r[obj_bonename]:
|
|
continue
|
|
#in automatic handles move only one of them
|
|
if get_side_auto(self, obj_bonename, frame, key) == 'handle_l':
|
|
continue
|
|
|
|
keyframe_coord = self.bones_keyframes_coords[obj_bonename][frame]
|
|
try:
|
|
dist_len = abs((handle_coords_r[frame] + distance - Vector(keyframe_coord)).length / (handle_coords_r[frame] - Vector(keyframe_coord)).length)
|
|
except ZeroDivisionError:
|
|
dist_len = None
|
|
handle_coords_r[frame] += distance
|
|
|
|
if self.bones_handles_types[obj_bonename][frame] == 'AUTO':
|
|
handle_coords_l[frame] = align_handles(handle_coords_l[frame], coord_3d, Vector(keyframe_coord), dist_len)
|
|
# handle_coords_l[frame] -= distance #For Auto Clamped move the oppposite direction
|
|
|
|
#Move the Left handle
|
|
for frame, coord_3d in handle_coords_l.items():
|
|
if obj_bonename not in self.bones_selected_handles_l:
|
|
continue
|
|
if frame not in self.bones_selected_handles_l[obj_bonename]:
|
|
continue
|
|
#in automatic handles move only one of them
|
|
if get_side_auto(self, obj_bonename, frame, key) == 'handle_r':
|
|
continue
|
|
|
|
keyframe_coord = self.bones_keyframes_coords[obj_bonename][frame]
|
|
#get the distance length before adding the distance to the handles
|
|
try:
|
|
dist_len = abs((handle_coords_l[frame] + distance - Vector(keyframe_coord)).length / (handle_coords_l[frame] - Vector(keyframe_coord)).length)
|
|
except ZeroDivisionError:
|
|
dist_len = None
|
|
handle_coords_l[frame] += distance
|
|
if self.bones_handles_types[obj_bonename][frame] == 'AUTO':
|
|
handle_coords_r[frame] = align_handles(handle_coords_r[frame], coord_3d, Vector(keyframe_coord), dist_len)
|
|
# handle_coords_r[frame] -= distance #For Auto Clamped move the oppposite direction
|
|
|
|
self.prev_event_value = event.value
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
return True
|
|
|
|
#Cancel the operation, usually with RIGHTMOUSE go back to initial coordinates
|
|
elif event.type == self.cancel_mouse and (self.press or self.scale or self.rotate):
|
|
obj_bonename, key, frame, selection, coord_3d = unpack_hover_frame(self)
|
|
# distance = Vector(coord_3d) - self.initial_coord
|
|
|
|
if key == 'keyframe':
|
|
for obj_bonename in selection.keys():
|
|
#get the coordinates from bones_keyframes_coords
|
|
keyframes_coords = self.bones_keyframes_coords[obj_bonename]
|
|
frames_distance = dict()
|
|
for frame, coord_3d in keyframes_coords.items():
|
|
if frame not in self.bones_selected_keyframes[obj_bonename]:
|
|
continue
|
|
|
|
# print(f'distance {distance} coord_3d {Vector(coord_3d)} initial_keyframe_coords {Vector(self.initial_keyframe_coords[obj_bonename][frame])}')
|
|
distance = Vector(coord_3d) - Vector(self.initial_keyframe_coords[obj_bonename][frame])
|
|
#get all the points from the curve of this bone
|
|
#get the index of the point based on the frame number
|
|
i = (round(frame - self.avz_frame_start[obj_bonename])) *3
|
|
coords = Vector(self.bones_points[obj_bonename][i : i+3])
|
|
if Vector(coord_3d) == coords:
|
|
self.bones_points[obj_bonename][i:i+3] = coords - distance
|
|
frames_distance.update({frame : distance * -1})
|
|
if self.bones_handles_right:
|
|
self.bones_handles_right[obj_bonename][frame] = self.bones_handles_right[obj_bonename][frame] - distance
|
|
self.bones_handles_left[obj_bonename][frame] = self.bones_handles_left[obj_bonename][frame] - distance
|
|
|
|
add_distance_to_points(self, obj_bonename, frames_distance)
|
|
else:
|
|
self.bones_handles_right = self.initial_handles_right
|
|
self.bones_handles_left = self.initial_handles_left
|
|
del self.initial_handles_right, self.initial_handles_left
|
|
|
|
self.press = False
|
|
self.scale = False
|
|
self.rotate = False
|
|
self.filter_axis = ''
|
|
# self.moving_frame = dict()
|
|
self.initial_coord = None
|
|
self.initial_keyframe_coords = None
|
|
self.hover_bone_frame = {'keyframe': None, 'handle_r' : None, 'handle_l' : None}
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
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)
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
return True
|
|
|
|
elif event.value == 'PRESS' and not self.ctrl:
|
|
#Setting up adding keyframe mode
|
|
self.ctrl = True
|
|
self.bones_allframes_coords = dict()
|
|
|
|
#Create self.bones_allframes_coords that is similiar to self.bones_keyframes_coords
|
|
for obj_bonename, points in self.bones_points.items():
|
|
for i in range(0, len(points), 3):
|
|
frame = ((i+3) / 3) + self.avz_frame_start[obj_bonename] - 1
|
|
if obj_bonename in self.bones_allframes_coords:
|
|
# if frame in self.bones_allframes_coords[obj_bonename]:
|
|
self.bones_allframes_coords[obj_bonename].update({frame : points[i : i+3]})
|
|
else:
|
|
self.bones_allframes_coords.update({obj_bonename : {frame : points[i : i+3]}})
|
|
|
|
coords_2d_update(self, context)
|
|
coords_2d, bone_frame = get_keyframe_coord2d(self)
|
|
if coords_2d in self.coords2d_bones_frames:
|
|
self.hover_bone_frame['keyframe'] = bone_frame
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
return True
|
|
|
|
return False
|
|
|
|
def add_frame_offset(obj, frame, add = False):
|
|
anim_data = obj.animation_data
|
|
if not anim_data.use_nla or not anim_data.use_tweak_mode:
|
|
return frame
|
|
if not len(anim_data.nla_tracks):
|
|
return frame
|
|
offset = frame - anim_data.nla_tweak_strip_time_to_scene(frame)
|
|
|
|
frame = frame + offset if add else frame - offset
|
|
|
|
return frame
|
|
|
|
def add_distance_to_points(self, obj_bonename, frames_distance):
|
|
|
|
for frame, distance in frames_distance.items():
|
|
frame_index = self.mp_bones_keys[obj_bonename].index(frame)
|
|
current_i = (round(frame - self.avz_frame_start[obj_bonename])) *3
|
|
if frame_index:
|
|
prev_key = round(self.mp_bones_keys[obj_bonename][frame_index -1], 2)
|
|
if prev_key < 0:
|
|
continue
|
|
prev_dist = frames_distance[prev_key] if prev_key in frames_distance else Vector((0,0,0))
|
|
prev_frames = int(frame - prev_key)
|
|
|
|
prev_i = (round(prev_key - self.avz_frame_start[obj_bonename])) *3
|
|
|
|
#Linear distribution of the points until the previous keyframe
|
|
t = np.linspace(prev_dist, distance, prev_frames, endpoint=False)
|
|
added_array = t.flatten()[3:]
|
|
#apply the keyframe position
|
|
self.bones_points[obj_bonename][prev_i + 3:current_i] += added_array
|
|
|
|
#Interpolatin for the next keyframe
|
|
if (frame_index + 1) >= len(self.mp_bones_keys[obj_bonename]):
|
|
continue
|
|
next_key = round(self.mp_bones_keys[obj_bonename][frame_index + 1], 2)
|
|
#if the next keyframe is also being moved then skip it
|
|
if next_key in frames_distance:
|
|
continue
|
|
next_frames = int(next_key - frame)
|
|
next_i = (round(next_key - self.avz_frame_start[obj_bonename])) *3
|
|
|
|
#Linear distribution of the points until the next keyframe
|
|
t = np.linspace(distance, Vector((0,0,0)), next_frames, endpoint=False)
|
|
#Flat the array to be 1 dimension
|
|
added_array = t.flatten()[3:]
|
|
self.bones_points[obj_bonename][current_i + 3 : next_i] += added_array
|
|
|
|
def draw_frames_callback_px(self, context):
|
|
font_id = 0 # XXX, need to find out how best to get this.
|
|
alpha = 0.75
|
|
|
|
# print(self.coords2d_bones_frames)
|
|
# for coords2d, bones_frames in self.coords2d_bones_frames.items():
|
|
for obj_bonename, keyframes_coords in self.bones_keyframes_coords.items():
|
|
for frame, coord_3d in keyframes_coords.items():
|
|
coords2d = bpy_extras.view3d_utils.location_3d_to_region_2d(context.region, context.region_data, coord_3d)
|
|
blf.position(font_id, coords2d[0],coords2d[1] + 10 ,0)
|
|
blf.size(font_id, 15)
|
|
blf.color(0, 1, 1, 0, alpha)
|
|
blf.draw(font_id, str(int(round(frame))) )
|
|
|
|
# Function to draw the motion path
|
|
def draw_motionpath_callback_px(self, context):
|
|
'''draw the motion path'''
|
|
batch_selected_keyframes = []
|
|
batch_handles_points = []
|
|
batch_handles_lines = []
|
|
selected_shader = gpu.shader.from_builtin("UNIFORM_COLOR")
|
|
atb = context.scene.animtoolbox
|
|
self.bones_keyframes_coords = dict()
|
|
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 = []
|
|
handles_vertices = []
|
|
keyframes_coords = dict()
|
|
|
|
col_lines = []
|
|
col_points = []
|
|
col_keys = []
|
|
col_handles_points = []
|
|
col_sel_keys = []
|
|
|
|
# frame_current = add_frame_offset(bpy.data.objects[obj_bonename[0]], context.scene.frame_current, add = False)
|
|
frame_current = context.scene.frame_current
|
|
|
|
#setup the hover attribute with the frame value. Either hover_keyframe, hover_handle_r or hover_handle_l
|
|
hover_keyframe = None
|
|
hover_handle_r = None
|
|
hover_handle_l = None
|
|
for key, value in self.hover_bone_frame.items():
|
|
if value is None:
|
|
continue
|
|
if obj_bonename in value:
|
|
if key == 'keyframe':
|
|
hover_keyframe = value[obj_bonename]
|
|
elif key == 'handle_r':
|
|
hover_handle_r = value[obj_bonename]
|
|
elif key == 'handle_l':
|
|
hover_handle_l = value[obj_bonename]
|
|
|
|
#iterate every 3 points, since every point has a 3 coordinate system
|
|
for i in range(0, len(points), 3):
|
|
|
|
#every 3 points are a vector of a frame
|
|
frame = ((i+3) / 3) + self.avz_frame_start[obj_bonename] - 1
|
|
#skip frames that are previous to the visual frames
|
|
if frame < self.frame_start[obj_bonename]:
|
|
continue
|
|
|
|
if atb.mp_frame_range == 'AROUND':
|
|
before = frame_current - atb.mp_before
|
|
after = frame_current + atb.mp_after
|
|
if frame < before or frame > after:
|
|
continue
|
|
|
|
coords = tuple(points[i : i+3])
|
|
vertices.append(coords)
|
|
#append keyframes for a separate shader
|
|
if frame in keyframes or frame == hover_keyframe:
|
|
key_vertices.append(coords)
|
|
# update the coordinates for each keyframe
|
|
keyframes_coords.update({frame : points[i : i+3]})
|
|
self.bones_keyframes_coords.update({obj_bonename : keyframes_coords})
|
|
|
|
#paint the current frame white
|
|
if frame == frame_current:
|
|
keyframe_color = (1.0, 1.0, 1.0, 1.0)
|
|
else:
|
|
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 = (0.0, 0.5, 1.0, 1.0)
|
|
else:
|
|
#color the frame that is going to be added or selected as orange
|
|
keyframe_color = (1.0, 0.4, 0.0, 1.0)
|
|
col_keys.append(keyframe_color)
|
|
|
|
#add the coordinates for the creating the squar for selected keyframes in the current bone
|
|
if obj_bonename in self.bones_selected_keyframes:
|
|
if frame in self.bones_selected_keyframes[obj_bonename]:
|
|
col_sel_keys.append((1.0, 1.0, 1.0, 0.1))
|
|
size = atb.mp_keyframe_scale
|
|
selected_coords, indices = draw_cube(coords, size)
|
|
|
|
#Add the keyframe control cage shader
|
|
batch_selected_keyframes.append(batch_for_shader(selected_shader, 'LINES', {"pos": selected_coords}, indices = indices))
|
|
|
|
if context.scene.animtoolbox.mp_handles and self.bones_handles_right:
|
|
#Add the handles to the selected keyframes
|
|
if frame not in self.bones_handles_right[obj_bonename]:
|
|
continue
|
|
handles_vertices.append(self.bones_handles_right[obj_bonename][frame])
|
|
if hover_handle_r == frame:
|
|
col_handles_points.append((1.0, 0.4, 0.2, 1.0))
|
|
else:
|
|
col_handles_points.append((1.0, 0.8, 0.2, 1.0))
|
|
|
|
#add the control cage to the right handles
|
|
if obj_bonename in self.bones_selected_handles_r:
|
|
if frame in self.bones_selected_handles_r[obj_bonename]:
|
|
selected_coords, indices = draw_sphere(self.bones_handles_right[obj_bonename][frame], size)
|
|
batch_selected_keyframes.append(batch_for_shader(selected_shader, 'LINES', {"pos": selected_coords}, indices = indices))
|
|
|
|
handles_vertices.append(self.bones_handles_left[obj_bonename][frame])
|
|
if hover_handle_l == frame:
|
|
col_handles_points.append((1.0, 0.4, 0.2, 1.0))
|
|
else:
|
|
col_handles_points.append((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]:
|
|
selected_coords, indices = draw_sphere(self.bones_handles_left[obj_bonename][frame], size)
|
|
batch_selected_keyframes.append(batch_for_shader(selected_shader, 'LINES', {"pos": selected_coords}, indices = indices))
|
|
|
|
#draw the line of the handles
|
|
handles_line = [self.bones_handles_right[obj_bonename][frame]] + [coords] + [self.bones_handles_left[obj_bonename][frame]]
|
|
batch_handles_lines.append(batch_for_shader(selected_shader, 'LINE_STRIP', {"pos": handles_line}))
|
|
|
|
#add the colors for the motion path lines and points
|
|
if frame > frame_current:
|
|
col_lines.append(atb.mp_color_after)
|
|
col_points.append(atb.mp_color_after)
|
|
elif frame < frame_current:
|
|
col_lines.append(atb.mp_color_before)
|
|
col_points.append(atb.mp_color_before)
|
|
else:
|
|
col_lines.append((atb.mp_color_after + atb.mp_color_before) * 0.5)
|
|
col_points.append((1.0, 1.0, 1.0))
|
|
|
|
if atb.mp_infront:
|
|
gpu.state.depth_test_set('NONE')
|
|
else:
|
|
gpu.state.depth_test_set('LESS_EQUAL')
|
|
|
|
#Create all the shaders
|
|
shader = gpu.shader.from_builtin("SMOOTH_COLOR")
|
|
if atb.mp_lines:
|
|
batch_line = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": col_lines})
|
|
if atb.mp_points:
|
|
batch_points = batch_for_shader(shader, 'POINTS', {"pos": vertices, "color": col_points})
|
|
batch_keyframes = batch_for_shader(shader, 'POINTS', {"pos": key_vertices, "color": col_keys})
|
|
if handles_vertices:
|
|
batch_handles_points = batch_for_shader(shader, 'POINTS', {"pos": handles_vertices, "color": col_handles_points})
|
|
|
|
#draw the path and lines
|
|
shader.bind()
|
|
gpu.state.line_width_set(2)
|
|
gpu.state.point_size_set(5)
|
|
if atb.mp_lines:
|
|
batch_line.draw(shader)
|
|
if atb.mp_points:
|
|
batch_points.draw(shader)
|
|
|
|
gpu.state.point_size_set(8)
|
|
batch_keyframes.draw(shader)
|
|
|
|
#draw selected keyframes box controller and handles
|
|
selected_shader.uniform_float("color", (1, 1, 1, 1))
|
|
if obj_bonename in self.bones_selected_keyframes:
|
|
for batch_selection in batch_selected_keyframes:
|
|
batch_selection.draw(selected_shader)
|
|
for batch_handle in batch_handles_lines:
|
|
batch_handle.draw(selected_shader)
|
|
|
|
# draw the points of the handles
|
|
if batch_handles_points:
|
|
batch_handles_points.draw(shader)
|
|
|
|
# if handles_vertices:
|
|
|
|
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(self, context):
|
|
'''dependency handler to use to avoid modal operator repeating on every mouse move'''
|
|
global value_update
|
|
value_update = True
|
|
|
|
def mp_frame_change(self, context):
|
|
'''dependency handler to use to avoid modal operator repeating on every mouse move'''
|
|
global frame_change
|
|
frame_change = True
|
|
|
|
def mp_redo_update(self, context):
|
|
'''dependency handler to use during redo'''
|
|
global undo
|
|
undo = True
|
|
|
|
def mp_undo_update(self, context):
|
|
'''dependency handler to use during undo'''
|
|
global undo
|
|
undo = True
|
|
|
|
def mp_undo_check(self, context):
|
|
global undo
|
|
if not undo:
|
|
return False
|
|
undo = False
|
|
|
|
# print('undo before deserialization', context.scene.animtoolbox.selected_keyframes)
|
|
if len(context.scene.animtoolbox.selected_keyframes):
|
|
self.bones_selected_keyframes = deserialize_dict(context.scene.animtoolbox.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)
|
|
return True
|
|
|
|
def remove_draw_handlers(self):
|
|
'''Remove motion path drawing and frames drawing'''
|
|
if hasattr(self, '_handle'):
|
|
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
|
del self._handle
|
|
if hasattr(self, 'draw_frames'):
|
|
bpy.types.SpaceView3D.draw_handler_remove(self.draw_frames, 'WINDOW')
|
|
del self.draw_frames
|
|
if 'mp_dh' in bpy.app.driver_namespace:
|
|
bpy.app.driver_namespace.pop('mp_dh')
|
|
if 'mp_df' in bpy.app.driver_namespace:
|
|
bpy.app.driver_namespace.pop('mp_df')
|
|
|
|
# Operator with modal and draw handler
|
|
class MotionPathOperator(bpy.types.Operator):
|
|
|
|
"""Creates a custom motion path"""
|
|
bl_idname = "object.motion_path_operator"
|
|
bl_label = "Motion Path Operator"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if context.scene.animtoolbox.motion_path:
|
|
return True
|
|
return True if context.selected_objects else False
|
|
# return len(context.selected_pose_bones)
|
|
|
|
@property
|
|
def range_value(self):
|
|
#the range distance around the keyframes for hovering, getting a dynamic value
|
|
return bpy.context.preferences.addons[__package__].preferences.keyframes_range
|
|
|
|
def invoke(self, context, event):
|
|
scene = context.scene
|
|
atb = scene.animtoolbox
|
|
|
|
if atb.motion_path:
|
|
atb.motion_path = False
|
|
return {'CANCELLED'}
|
|
|
|
if context.selected_pose_bones is None and context.selected_objects is None:
|
|
atb.motion_path = False
|
|
return {'CANCELLED'}
|
|
atb.motion_path = True
|
|
|
|
global undo, value_update, 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
|
|
|
|
#Check if we use motion path in pose mode or object mode
|
|
self.posemode = True if context.selected_pose_bones else False
|
|
|
|
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 = ''
|
|
|
|
#check for right or left mouse selection
|
|
preferences = context.window_manager.keyconfigs.default.preferences
|
|
if preferences:
|
|
self.select_mouse = preferences.select_mouse + 'MOUSE'
|
|
self.cancel_mouse = 'RIGHTMOUSE' if self.select_mouse == 'LEFTMOUSE' else 'LEFTMOUSE'
|
|
else:
|
|
self.select_mouse, self.cancel_mouse = 'LEFTMOUSE', 'RIGHTMOUSE'
|
|
|
|
self.hover_bone_frame = {'keyframe': None, 'handle_r' : None, 'handle_l' : None} #get the bone and frame of the keyframe that change color while hovering
|
|
# self.hover_handle = {'r' : None, 'l' : None} #get the bone and frame of the handle that change color while hovering
|
|
|
|
self.prev_event_value = None # records the previous mouse event to see if it was just a click and release for selecting
|
|
# self.moving_frame = dict() #frame number of the keyframe that is selected and being moved
|
|
self.initial_coord = None #the starting point of the coords when starting to move them with g or mouse press
|
|
self.prev_view_matrix = context.region_data.view_matrix.copy() #caluclating 2d coordinates only when view and window matrixchange
|
|
self.prev_window_matrix = context.region_data.window_matrix.copy()
|
|
# self.range_value = 15 #the range distance around the keyframes for hovering
|
|
|
|
self.timer_tick_counter = 0 #using a timer while motion paths are updated
|
|
self.timer = None
|
|
|
|
self.items = context.selected_pose_bones if self.posemode else context.selected_objects
|
|
|
|
#get rid of the items that don't have animation data
|
|
self.items = [item for item in self.items if item.id_data.animation_data]
|
|
|
|
if not self.items:
|
|
atb.motion_path = False
|
|
return {'CANCELLED'}
|
|
|
|
self.frame_range_type = atb.mp_frame_range
|
|
|
|
self.use_preview_range = scene.use_preview_range
|
|
|
|
self.frame_start = dict()
|
|
self.frame_end = dict()
|
|
#extra start property incase we need to calculate the path before the official start
|
|
self.avz_frame_start = dict()
|
|
|
|
#All the points of the motion path per bone
|
|
self.bones_points = dict()
|
|
#All the coordinates of the keyframes per bone, it has a sub dictionary of keyframes_coords with the frame number and it's coordinates
|
|
self.bones_keyframes_coords = dict()
|
|
#a copy of the keyframe coords before they start to move
|
|
self.initial_keyframe_coords = None
|
|
#check mark which bones are cyclic
|
|
self.bones_cyclic = dict()
|
|
#coordinates for all the frames when using the coordinates
|
|
self.bones_allframes_coords = dict()
|
|
#The 2D coordinates of the keyframes per bone, , it has a sub dictionary of keyframes_coords with the frame number and it's coordinates
|
|
self.coords2d_bones_frames = dict()
|
|
#The 2D coordinates of the keyframes per bone, , it has a sub dictionary of handles coords with the frame number and it's coordinates
|
|
self.coords2d_handles_r = dict()
|
|
self.coords2d_handles_l = dict()
|
|
|
|
self.coords2d_ranges = []
|
|
#Dictionary of selected bones and the frame numbers of the selected keyframes
|
|
self.bones_selected_keyframes = dict()
|
|
self.bones_selected_handles_r = dict()
|
|
self.bones_selected_handles_l = dict()
|
|
|
|
#dictinary of bones, frames and the handles coordinates
|
|
self.bones_handles_right = dict()
|
|
self.bones_handles_left = dict()
|
|
self.mp_handles = atb.mp_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 = []
|
|
# bones_length = []
|
|
|
|
nfr = context.preferences.edit.use_negative_frames
|
|
|
|
for item in self.items:
|
|
obj_bonename = (item.id_data.name, item.name)
|
|
self.mp_bone_names.append(obj_bonename)
|
|
|
|
obj = item.id_data
|
|
|
|
if self.posemode:
|
|
avz_mp = obj.pose.animation_visualization.motion_path
|
|
else:
|
|
avz_mp = obj.animation_visualization.motion_path
|
|
|
|
avz_mp.range ='KEYS_ALL' if atb.mp_frame_range == 'AROUND' else atb.mp_frame_range
|
|
|
|
#Camera space relevant only from version 4.1 - currently disabled
|
|
if hasattr(avz_mp, 'bake_in_camera_space'):
|
|
if avz_mp.bake_in_camera_space:
|
|
avz_mp.bake_in_camera_space = False
|
|
|
|
#assign the frame range per object
|
|
frame_start, frame_end = get_frame_range(scene, obj)
|
|
self.frame_start.update({obj_bonename : frame_start})
|
|
self.frame_end.update({obj_bonename : frame_end})
|
|
update_avz_frame_start(self, context, obj_bonename)
|
|
|
|
#Negative frame range, needs to be checked in case frame start is minus
|
|
turn_nfr_on(context.preferences, self.avz_frame_start[obj_bonename])
|
|
|
|
avz_mp.frame_start, avz_mp.frame_end = self.avz_frame_start[obj_bonename], frame_end
|
|
|
|
#store bone names in case they were changed
|
|
item['atb_mp_name'] = item.name
|
|
item.id_data['atb_mp_name'] = item.id_data.name
|
|
|
|
self.current_selected_items = context.selected_pose_bones if self.posemode else context.selected_objects
|
|
self.frame_current = scene.frame_current_final
|
|
|
|
matrix_differece(self, context)
|
|
|
|
self.current_bones_keys = get_current_bones_keys(self)
|
|
self.obj_layer_properties = get_layer_properties(self.items)
|
|
#('value update in globals ', 'value_update' in globals())
|
|
value_update = True
|
|
# mp_frame_range_change(self, context)
|
|
|
|
get_mp_points(self, context)
|
|
|
|
#return negative frame range to what it was
|
|
if context.preferences.edit.use_negative_frames != nfr:
|
|
context.preferences.edit.use_negative_frames = nfr
|
|
|
|
#assign the draw handler
|
|
context.window_manager.modal_handler_add(self)
|
|
|
|
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 atb.mp_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)
|
|
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
|
|
bpy.ops.ed.undo_push(message = 'Initialize Motion Path')
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
|
|
# using a timer when creating a motion path so that the hotkey G doesnt respond before the path is created
|
|
# if event.type == 'TIMER' and self.timer:
|
|
# if self.timer_tick_counter < 3:
|
|
# self.timer_tick_counter += 1
|
|
# # print('timer counter')
|
|
# return {'RUNNING_MODAL'}
|
|
# else:
|
|
# context.window_manager.event_timer_remove(self.timer)
|
|
# self.timer_tick_counter = 0
|
|
# self.timer = None
|
|
#quit the modal operators
|
|
|
|
# if event.type in {'ESC'}:
|
|
# context.scene.animtoolbox.motion_path = False
|
|
atb = context.scene.animtoolbox
|
|
#Quit the draw handler
|
|
if not atb.motion_path:
|
|
quit_mp(self, context)
|
|
return {'FINISHED'}
|
|
|
|
try:
|
|
global value_update
|
|
|
|
if mp_undo_check(self, context):
|
|
return {'PASS_THROUGH'}
|
|
|
|
if check_bone_names(self, context):
|
|
return {'PASS_THROUGH'}
|
|
|
|
region = context.region
|
|
self.mouse_x = event.mouse_x - region.x
|
|
self.mouse_y = event.mouse_y - region.y
|
|
|
|
if compare_bone_keys(self,context, event):
|
|
return {'PASS_THROUGH'}
|
|
|
|
compare_obj_layers(self, context, event)
|
|
|
|
frames = mp_frame_range_change(self, context)
|
|
if frames:
|
|
update_mp_points(self, context, frames, fr_update = True)
|
|
return {'PASS_THROUGH'}
|
|
|
|
if not region.x <= event.mouse_x <= (region.x + region.width) or not region.y <= event.mouse_y <= (region.y + region.height):
|
|
value_update = False
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
global frame_change
|
|
|
|
#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
|
|
if atb.mp_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 atb.mp_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 bpy.app.driver_namespace:
|
|
bpy.app.driver_namespace.pop('mp_df')
|
|
|
|
#when going out of pose mode remove the draw handler
|
|
if context.mode != 'POSE' and self.posemode:
|
|
# for obj_bonename in self.mp_bone_names:
|
|
# objname, bonename = obj_bonename
|
|
# obj = context.view_layer.objects[objname]
|
|
|
|
remove_draw_handlers(self)
|
|
return{'PASS_THROUGH'}
|
|
|
|
if event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'} and event.value == 'PRESS':
|
|
self.shift = True
|
|
elif event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'} and event.value == 'RELEASE':
|
|
self.shift = False
|
|
|
|
if zoom_keyframes(self, context, event):
|
|
return {'RUNNING_MODAL'}
|
|
|
|
if edit_motion_path_update(self, context, event):
|
|
return {'RUNNING_MODAL'}
|
|
|
|
if self.press or self.scale or self.rotate:
|
|
return {'RUNNING_MODAL'}
|
|
|
|
#check if the bone selection was changed
|
|
selected_items = context.selected_pose_bones if self.posemode else context.selected_objects
|
|
if self.current_selected_items != selected_items:
|
|
#need to checek this condition
|
|
if selected_items not in self.current_selected_items:
|
|
self.current_selected_items = selected_items
|
|
self.current_bones_keys = get_current_bones_keys(self)
|
|
return {'PASS_THROUGH'}
|
|
|
|
self.current_selected_items = context.selected_pose_bones
|
|
return {'PASS_THROUGH'}
|
|
|
|
value_update = False
|
|
|
|
mp_handles_on_off(self, context)
|
|
|
|
mp_set_handle_type(self, context, event)
|
|
mp_set_interpolation(self, context, event)
|
|
#when pressing around the motion path it should run modal to avoid selecting and deselecting controls
|
|
# if (int(self.mouse_x), int(self.mouse_y)) in self.coords2d_ranges and self.press:
|
|
# return {'RUNNING_MODAL'}
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
except Exception as e:
|
|
# Log the error
|
|
print("Error:", e)
|
|
context.scene.animtoolbox.motion_path = False
|
|
quit_mp(self, context)
|
|
self.report({'ERROR'}, str(e) + '. Quitting Motion Paths')
|
|
traceback.print_exc()
|
|
return {'CANCELLED'}
|
|
|
|
def quit_mp(self, context):
|
|
|
|
remove_draw_handlers(self)
|
|
|
|
# for obj, self.mp_bone_names in self.mp_obj_bone_names.items():
|
|
for obj_bonename in self.mp_bone_names:
|
|
objname, bonename = obj_bonename
|
|
if objname not in context.view_layer.objects:
|
|
print(objname, 'not found in objects')
|
|
continue
|
|
obj = context.view_layer.objects[objname]
|
|
if 'atb_mp_name' in obj.keys():
|
|
del obj['atb_mp_name']
|
|
if not self.posemode:
|
|
continue
|
|
bone = obj.pose.bones[bonename]
|
|
if 'atb_mp_name' in bone.keys():
|
|
del bone['atb_mp_name']
|
|
|
|
#remove dependecy handler
|
|
bpy.app.handlers.depsgraph_update_post.remove(mp_value_update)
|
|
bpy.app.handlers.frame_change_post.remove(mp_frame_change)
|
|
bpy.app.handlers.redo_pre.remove(mp_redo_update)
|
|
bpy.app.handlers.undo_pre.remove(mp_undo_update)
|
|
global undo
|
|
if 'undo' in globals():
|
|
del undo
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
|
|
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)
|
|
#get current bones keys to avoid recalculation of path points
|
|
self.current_bones_keys = get_current_bones_keys(self)
|
|
interpolation = None
|
|
|
|
if event.type != 'T' or event.value != 'PRESS' or not self.ctrl:
|
|
return
|
|
self.ctrl = False
|
|
for win in context.window_manager.windows:
|
|
for area in win.screen.areas:
|
|
if area.type != 'VIEW_3D':
|
|
continue
|
|
if area.width > event.mouse_x > area.x and area.y + area.height > event.mouse_y > area.y:
|
|
context.window_manager.popup_menu(draw_interpolation_menu, title = 'Set Interpolation')
|
|
return
|
|
|
|
def mp_set_handle_type(self, context, event):
|
|
|
|
global handle_type
|
|
if handle_type:
|
|
frames = update_mp_handle_types(self, context, handle_type)
|
|
if not frames:
|
|
return
|
|
update_mp_points(self, context, frames)
|
|
#get current bones keys to avoid recalculation of path points
|
|
self.current_bones_keys = get_current_bones_keys(self)
|
|
handle_type = None
|
|
|
|
if event.type != 'V' or event.value != 'PRESS':
|
|
return
|
|
if not self.bones_selected_keyframes:
|
|
return
|
|
for win in context.window_manager.windows:
|
|
for area in win.screen.areas:
|
|
if area.type != 'VIEW_3D':
|
|
continue
|
|
if area.width > event.mouse_x > area.x and area.y + area.height > event.mouse_y > area.y:
|
|
context.window_manager.popup_menu(draw_handle_type_menu, title = 'Set Keyframe Handle Type')
|
|
return
|
|
|
|
def draw_handle_type_menu(self, context):
|
|
'''Draw the menu for the handle types of the selected keyframes'''
|
|
layout = self.layout
|
|
layout.prop(context.scene.animtoolbox, 'mp_handle_types', expand = True)
|
|
|
|
def draw_interpolation_menu(self, context):
|
|
layout = self.layout
|
|
layout.prop(context.scene.animtoolbox, 'mp_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.mp_handle_types
|
|
|
|
def update_interpolation_prop(self, context):
|
|
global interpolation
|
|
interpolation = self.mp_interpolation
|
|
|
|
def turn_nfr_on(preferences, start):
|
|
'''Motion path doesnt draw negative frames if it's turned off and we are using MANUAL'''
|
|
edit = preferences.edit
|
|
nfr = edit.use_negative_frames
|
|
|
|
#if it's already turned on
|
|
if start >= 0 or nfr:
|
|
return nfr
|
|
|
|
edit.use_negative_frames = True
|
|
|
|
return nfr
|
|
|
|
def get_frame_range(scene, obj):
|
|
# print('get_frame_range')
|
|
atb = scene.animtoolbox
|
|
if atb.mp_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 atb.mp_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 atb.mp_frame_range == 'MANUAL':
|
|
frame_start = atb.bake_frame_start
|
|
frame_end = atb.bake_frame_end
|
|
elif atb.mp_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 = max([scene_start, keys_start])
|
|
frame_end = min([scene_end, keys_end])
|
|
|
|
return frame_start, frame_end
|
|
|
|
def mp_frame_range_change(self, context):
|
|
'''Updating the motion path when changing the frame range'''
|
|
scene = context.scene
|
|
atb = scene.animtoolbox
|
|
|
|
|
|
global value_update #checks dependency graph updates
|
|
# print('frame_start', list(self.frame_start.values()))
|
|
if atb.mp_frame_range == 'SCENE':
|
|
if scene.use_preview_range != self.use_preview_range:
|
|
value_update = True
|
|
# if scene.use_preview_range:
|
|
# if scene.frame_preview_start != self.frame_start or scene.frame_preview_end != self.frame_end:
|
|
# value_update= True
|
|
if scene.use_preview_range:
|
|
if scene.frame_preview_start not in list(self.frame_start.values()) or scene.frame_preview_end not in list(self.frame_end.values()):
|
|
value_update= True
|
|
|
|
if not value_update:
|
|
return False
|
|
|
|
# print('mp_frame_range_change ', value_update)
|
|
nfr = context.preferences.edit.use_negative_frames
|
|
frames = []
|
|
# return_value = False
|
|
#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] = atb.bake_frame_start = frame_start #
|
|
self.frame_end[obj_bonename] = atb.bake_frame_end = frame_end #
|
|
self.frame_range_type = atb.mp_frame_range #= avz_mp.range
|
|
self.use_preview_range = scene.use_preview_range
|
|
update_avz_frame_start(self, context, obj_bonename)
|
|
turn_nfr_on(context.preferences, self.avz_frame_start[obj_bonename])
|
|
avz_mp.frame_start = frame_start
|
|
avz_mp.frame_end = frame_end
|
|
|
|
if obj_bonename not in self.bones_points:
|
|
continue
|
|
|
|
#update the begining of the motion path
|
|
if self.avz_frame_start[obj_bonename] != prev_frame_start:
|
|
if self.avz_frame_start[obj_bonename] > prev_frame_start:
|
|
remove_points = (self.avz_frame_start[obj_bonename] - prev_frame_start)*3
|
|
self.bones_points[obj_bonename] = self.bones_points[obj_bonename][remove_points:]
|
|
|
|
elif self.avz_frame_start[obj_bonename] < prev_frame_start:
|
|
extra_points = np.zeros(abs(prev_frame_start - self.avz_frame_start[obj_bonename])*3)
|
|
self.bones_points[obj_bonename] = np.concatenate((extra_points, self.bones_points[obj_bonename]))
|
|
frames += list(range(self.avz_frame_start[obj_bonename], (prev_frame_start)))
|
|
|
|
#update the end of the motion path
|
|
if self.frame_end[obj_bonename] != prev_frame_end:
|
|
if self.frame_end[obj_bonename] < prev_frame_end:
|
|
remove_points = (prev_frame_end - self.frame_end[obj_bonename])*3
|
|
self.bones_points[obj_bonename] = self.bones_points[obj_bonename][:-remove_points]
|
|
|
|
elif self.frame_end[obj_bonename] > prev_frame_end:
|
|
extra_frames = self.frame_end[obj_bonename] - prev_frame_end
|
|
extra_points = np.zeros(abs(extra_frames)*3)
|
|
self.bones_points[obj_bonename] = np.concatenate((self.bones_points[obj_bonename], extra_points))
|
|
frames += (range(prev_frame_end, self.frame_end[obj_bonename]))
|
|
|
|
if context.preferences.edit.use_negative_frames != nfr:
|
|
context.preferences.edit.use_negative_frames = nfr
|
|
|
|
value_update = True
|
|
return frames
|
|
|
|
def version_frame_end(frame_end):
|
|
'''Frame end for motion paths changed from version 4.1 in one frame'''
|
|
if bpy.app.version < (4, 1, 0):
|
|
frame_end += 1
|
|
return frame_end
|
|
|
|
def update_mp_bones_keys(self, item, obj_bonename):
|
|
|
|
frames_with_offset = map(lambda frame: round(add_frame_offset(item.id_data, frame), 2), get_bone_keyframes(item)[::2])
|
|
frames_with_offset = sorted(set(frames_with_offset))
|
|
self.mp_bones_keys.update({obj_bonename : frames_with_offset})
|
|
|
|
def update_avz_frame_start(self, context, obj_bonename):
|
|
'''an extra start property in case the motion path starts between keyframes and is cyclic
|
|
I need to calculate everything coming from the keyframe'''
|
|
objname, bonename = obj_bonename
|
|
obj = context.view_layer.objects[objname]
|
|
|
|
item = obj.pose.bones[bonename] if self.posemode else obj
|
|
|
|
# if obj_bonename in self.mp_bones_keys:
|
|
# frames = sorted(set(self.mp_bones_keys[obj_bonename]))
|
|
# else:
|
|
update_mp_bones_keys(self, item, obj_bonename)
|
|
frames = self.mp_bones_keys[obj_bonename]
|
|
frame_start = self.frame_start[obj_bonename]
|
|
#in case there is no keyframes
|
|
if not frames:
|
|
self.avz_frame_start.update({obj_bonename: frame_start})
|
|
return
|
|
|
|
|
|
if self.frame_start[obj_bonename] in frames:
|
|
self.avz_frame_start.update({obj_bonename: frame_start})
|
|
return
|
|
|
|
i = bisect.bisect_left(frames, frame_start)
|
|
|
|
avz_frame_start = round(frames[i-1]) if i else round(frames[i])
|
|
|
|
#this should apply only when it's smaller then the actual frame start
|
|
if avz_frame_start > frame_start:
|
|
avz_frame_start = frame_start
|
|
|
|
self.avz_frame_start.update({obj_bonename: avz_frame_start})
|
|
|
|
|
|
def frame_range_reset(self, context):
|
|
#Get the original frame range back to Blender's motion path
|
|
obj_bonenames = {obj_bonename for obj_bonename in self.mp_bones_keys.keys()}
|
|
for obj_bone in obj_bonenames:
|
|
obj = context.view_layer.objects[obj_bone[0]]
|
|
avz_mp = obj.pose.animation_visualization.motion_path if self.posemode else obj.animation_visualization.motion_path
|
|
avz_mp.frame_start = self.avz_frame_start[obj_bone]
|
|
avz_mp.frame_end = self.frame_end[obj_bone]
|
|
|
|
def available_for_update(modifiers):
|
|
'''check if the values of the modifiers are equal'''
|
|
if not len(modifiers):
|
|
return True
|
|
if len(modifiers) < 3:
|
|
return False
|
|
props = ['cycles_after', 'cycles_before', 'frame_end', 'frame_start', 'mode_after', 'mode_before','use_restricted_range']
|
|
#list of all the dictionaries
|
|
mod_dicts = []
|
|
|
|
for mod in modifiers:
|
|
mod_props = dict()
|
|
for prop in props:
|
|
value = getattr(mod, prop)
|
|
mod_props.update({prop : value})
|
|
mod_dicts.append(mod_props)
|
|
if mod_dicts[0] == mod_dicts[1] == mod_dicts[2]:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def restrict_range(start_cyclic, end_cyclic, restrict_start_point, restrict_end_point):
|
|
if start_cyclic < restrict_start_point:
|
|
start_cyclic = restrict_start_point
|
|
if end_cyclic > restrict_end_point:
|
|
end_cyclic = restrict_end_point
|
|
if end_cyclic < restrict_start_point or restrict_start_point > restrict_end_point:
|
|
start_cyclic = end_cyclic = 0
|
|
|
|
return start_cyclic, end_cyclic
|
|
|
|
def repeat_mp_difference(self, obj_bonename, repeat, difference, length, start, end, mod):
|
|
|
|
for r in range(1, repeat + 1):
|
|
#the index of the points where the values are being added
|
|
start_repeat = start + length * r *3
|
|
end_repeat = end + length * r * 3
|
|
if end_repeat < 0:
|
|
continue
|
|
if start_repeat < 0:
|
|
#the cyclic starts before 0, reseting to 0 and slicing it from the difference
|
|
difference = difference[-start_repeat:]
|
|
start_repeat = 0
|
|
# continue
|
|
|
|
diff_add = difference
|
|
|
|
# Ensure the frame range is within the restriced range
|
|
if mod.use_restricted_range:
|
|
frame_start_repeat = ((start_repeat+3) / 3) + self.avz_frame_start[obj_bonename] - 1
|
|
frame_end_repeat = ((end_repeat+3) / 3) + self.avz_frame_start[obj_bonename] - 1
|
|
|
|
adjusted_start = max(frame_start_repeat, mod.frame_start)
|
|
adjusted_end = min(frame_end_repeat, mod.frame_end + 1)
|
|
if not mod.frame_start < adjusted_start < mod.frame_end and not mod.frame_start < adjusted_end < mod.frame_end:
|
|
continue
|
|
|
|
# adding the restricted frames to the frame range
|
|
start_adjust = round((adjusted_start - frame_start_repeat)*3)
|
|
end_adjust = round((frame_end_repeat - adjusted_end)*3)
|
|
start_repeat = int(round(start_repeat + start_adjust))
|
|
end_repeat = int(round(end_repeat - end_adjust))
|
|
#matching the difference array length to the restricted frame range
|
|
diff_add = difference[max(0, start_adjust) : max(0, len(diff_add) - end_adjust)]
|
|
|
|
if len(self.bones_points[obj_bonename][start_repeat : end_repeat]) != len(diff_add):
|
|
#in case the cyclic is not complete and the last repeat is missing some frames from the difference array
|
|
i = len(self.bones_points[obj_bonename][start_repeat : end_repeat]) - len(diff_add)
|
|
diff_add = diff_add[:i]
|
|
|
|
# Debug logging
|
|
# print(f"Repeat #{r}: start={start_repeat}, end={end_repeat}, diff_add={len(diff_add)} end-start {end_repeat - start_repeat}")
|
|
|
|
self.bones_points[obj_bonename][start_repeat : end_repeat] += diff_add
|
|
|
|
def update_mp_points(self, context, frames, included = None, key = None, coord_3d = None, fr_update = False):
|
|
'''change the frame range for the motion path to update only specific points'''
|
|
# print('update_mp_points')
|
|
frame_range = []
|
|
bones_cycles = dict()
|
|
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
|
|
|
|
#get all the keyframes from all the arrays and check what is the last previous and next value
|
|
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
path = get_location_path(self.posemode, obj, bonename)
|
|
|
|
modifiers = []
|
|
i_keyframes = dict()
|
|
|
|
#get all the cyclic modifiers
|
|
for i in range(3):
|
|
fcu = fcurves.find(data_path = path, index = i)
|
|
if fcu is None:
|
|
continue
|
|
if len(fcu.modifiers):
|
|
for mod in fcu.modifiers:
|
|
if mod.type != 'CYCLES' or mod.mute:
|
|
continue
|
|
|
|
#Influence works in a strange way, recalcuating the whole path in this case
|
|
if mod.use_influence and mod.influence < 1:
|
|
get_mp_points(self, context)
|
|
return
|
|
#Blend mode works in a strange way, recalcuating the whole path in this case
|
|
if mod.use_restricted_range:
|
|
if mod.blend_in or mod.blend_out:
|
|
get_mp_points(self, context)
|
|
return
|
|
#Currently including only none and repeat
|
|
if mod.mode_before not in {'NONE', 'REPEAT'} or mod.mode_after not in {'NONE', 'REPEAT'}:
|
|
get_mp_points(self, context)
|
|
return
|
|
|
|
modifiers.append(mod)
|
|
|
|
if not len(fcu.keyframe_points):
|
|
continue
|
|
keyframes = np.zeros(len(fcu.keyframe_points)*2)
|
|
# keyframes = [0]* (len(fcu.keyframe_points)*2)
|
|
fcu.keyframe_points.foreach_get('co', keyframes)
|
|
#get the frames with the frame offset
|
|
keyframes = list(map(lambda frame: add_frame_offset(obj, frame), keyframes[::2]))
|
|
i_keyframes.update({i : keyframes})
|
|
|
|
#if it's only partly cyclic then recalculate the whole motion path
|
|
if not available_for_update(modifiers):
|
|
get_mp_points(self, context)
|
|
return
|
|
|
|
if modifiers:
|
|
bones_cycles.update({obj_bonename : modifiers})
|
|
|
|
for keyframes in i_keyframes.values():
|
|
keyframes = np.round(keyframes, 2)
|
|
for frame in frames:
|
|
prev_i = bisect.bisect_left(keyframes, frame)
|
|
if prev_i:
|
|
prev_i -= 1
|
|
prev_keyframe = keyframes[prev_i]
|
|
#get frames that are pre first keyframe
|
|
if frame < keyframes[0]:
|
|
prev_keyframe = frame
|
|
|
|
next_i = bisect.bisect_right(keyframes, frame)
|
|
if next_i >= len(keyframes):
|
|
#get frames that are after the last keyframe
|
|
next_keyframe = frame
|
|
else:
|
|
next_keyframe = keyframes[next_i]
|
|
|
|
frame_range.append(prev_keyframe)
|
|
frame_range.append(next_keyframe)
|
|
|
|
#usually breaks when selecting a layer with no keyframes
|
|
if not frame_range:
|
|
get_mp_points(self, context)
|
|
return
|
|
|
|
#creating a copy for cyclic points to be used after the distance array was added to the first and last keys
|
|
cyclic_points = copy.deepcopy(self.bones_points)
|
|
|
|
if key == 'keyframe':
|
|
#applying the distance after and before the last and first keyframes
|
|
distance = coord_3d - self.initial_coord
|
|
if obj_bonename in self.bones_selected_keyframes:
|
|
#all the keyframes that has distance restored used for inbetween frames
|
|
frames_distance = dict()
|
|
for frame in self.bones_selected_keyframes[obj_bonename]:
|
|
restore_point = (round(frame - self.avz_frame_start[obj_bonename])) *3
|
|
if self.scale or self.rotate:
|
|
coord = self.bones_keyframes_coords[obj_bonename][frame]
|
|
distance = coord - self.initial_keyframe_coords[obj_bonename][frame]
|
|
|
|
if obj_bonename in bones_cycles:
|
|
#restoring previous points before repeating them in cyclic fcurves back to the initial coordinates
|
|
self.bones_points[obj_bonename][restore_point : restore_point+3] -= distance
|
|
frames_distance.update({frame : distance * -1})
|
|
#Adding the distance to the rest of the points before and after the first keys
|
|
if frame == self.mp_bones_keys[obj_bonename][-1]:
|
|
#adding distance to all frames after the last keyframe
|
|
length = len(self.bones_points[obj_bonename][restore_point+3:])/3
|
|
distance_array = np.tile(np.array(distance), round(length))
|
|
self.bones_points[obj_bonename][restore_point+3:] += distance_array
|
|
|
|
elif frame == self.mp_bones_keys[obj_bonename][0]:
|
|
#adding distance to all frames before the first keyframe
|
|
length = len(self.bones_points[obj_bonename][:restore_point])/3
|
|
distance_array = np.tile(np.array(distance), round(length))
|
|
self.bones_points[obj_bonename][:restore_point] += distance_array
|
|
|
|
#reverse the distance for all the inbetween points
|
|
add_distance_to_points(self, obj_bonename, frames_distance)
|
|
|
|
#Temporarly change the frame range around the modified keyframes
|
|
temp_start = min(frame_range)
|
|
temp_end = max(frame_range) #adding one frame to include the last frame in the calculation
|
|
temp_end = version_frame_end(temp_end)
|
|
|
|
if temp_start < self.avz_frame_start[obj_bonename]:
|
|
temp_start = self.avz_frame_start[obj_bonename]
|
|
|
|
if temp_end > self.frame_end[obj_bonename]:
|
|
temp_end = self.frame_end[obj_bonename]
|
|
|
|
avz_mp.frame_start = round(temp_start)
|
|
avz_mp.frame_end = round(temp_end)
|
|
|
|
stored_bones_points = self.bones_points.copy()
|
|
# print('redrawing motion path ', avz_mp.frame_start, avz_mp.frame_end)
|
|
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
|
|
|
|
end_key = max(self.mp_bones_keys[obj_bonename])
|
|
start_key = min(self.mp_bones_keys[obj_bonename])
|
|
length = round(end_key - start_key)
|
|
|
|
#start and end points
|
|
if bpy.app.version < (4, 1, 0):
|
|
avz_mp.frame_end -= 1
|
|
start_p = (avz_mp.frame_start - self.avz_frame_start[obj_bonename]) * 3
|
|
end_p = (avz_mp.frame_end - self.avz_frame_start[obj_bonename]) * 3
|
|
|
|
if bpy.app.version >= (4, 1, 0) :
|
|
end_p += 3
|
|
if end_p > len(stored_bones_points[obj_bonename]):
|
|
end_p = len(stored_bones_points[obj_bonename])
|
|
|
|
if self.bones_points[obj_bonename].shape[0] != stored_bones_points[obj_bonename][start_p : end_p].shape[0]:
|
|
#checking if length is not equal and recalculating the whole path if it's not
|
|
print(f'length is not equal for {obj_bonename}')
|
|
print(f'selected objects {context.selected_objects} bones {context.selected_pose_bones}')
|
|
print(f'self.bones_points {self.bones_points[obj_bonename].shape[0]} stored_bones_points {stored_bones_points[obj_bonename][start_p : end_p].shape[0]}')
|
|
frame_range_reset(self, context)
|
|
get_mp_points(self, context, included)
|
|
return
|
|
|
|
#Get the difference between the old points and the new points to use in cyclic modifier
|
|
difference = self.bones_points[obj_bonename] - stored_bones_points[obj_bonename][start_p : end_p]
|
|
# print('difference', difference, stored_bones_points[obj_bonename][start : end], self.bones_points[obj_bonename])
|
|
#Replace the part of the old points with the new points in a copy of all the points
|
|
# print(f'stored points {len(stored_bones_points[obj_bonename][start_p : end_p])} new points {len(self.bones_points[obj_bonename])}')
|
|
stored_bones_points[obj_bonename][start_p : end_p] = self.bones_points[obj_bonename]
|
|
#Get the copy of the points
|
|
self.bones_points[obj_bonename] = stored_bones_points[obj_bonename]
|
|
|
|
#skip if not cyclic
|
|
if obj_bonename not in bones_cycles:
|
|
continue
|
|
|
|
mod = bones_cycles[obj_bonename][0]
|
|
|
|
if start_key < self.avz_frame_start[obj_bonename]:
|
|
start_key = self.avz_frame_start[obj_bonename]
|
|
|
|
repeat_after = math.ceil((self.frame_end[obj_bonename] - end_key)/length)
|
|
repeat_before = math.ceil((start_key - self.avz_frame_start[obj_bonename])/length)
|
|
|
|
# checking if it starts or finish earlier based on the cyclic settings
|
|
cycles_before = mod.cycles_before
|
|
if cycles_before:
|
|
if repeat_before > cycles_before:
|
|
repeat_before = cycles_before
|
|
elif mod.mode_before == 'NONE':
|
|
repeat_before = 0
|
|
|
|
cycles_after = mod.cycles_after
|
|
if cycles_after:
|
|
if repeat_after > cycles_after:
|
|
repeat_after = cycles_after
|
|
elif mod.mode_after == 'NONE':
|
|
repeat_after = 0
|
|
|
|
#getting the point for the restrict frame range
|
|
restrict_start_point = (round(mod.frame_start - self.avz_frame_start[obj_bonename])) *3
|
|
restrict_end_point = (round(mod.frame_end - self.avz_frame_start[obj_bonename])) *3 + 3
|
|
|
|
#Restoring cyclic animation before distance array was added to all the before and after values
|
|
start_after_cyclic = (round(end_key - self.avz_frame_start[obj_bonename])) *3
|
|
end_after_cyclic = round(start_after_cyclic + length * (repeat_after -1 )) *3 + 3
|
|
|
|
end_before_cyclic = (round(start_key - self.avz_frame_start[obj_bonename])) *3# + 3
|
|
start_before_cyclic = round(end_before_cyclic - length * (repeat_before)) * 3
|
|
|
|
#Add the difference to cyclic before
|
|
if avz_mp.frame_end != self.frame_end[obj_bonename]:
|
|
#Apply the restrict range
|
|
if mod.use_restricted_range:
|
|
start_after_cyclic, end_after_cyclic = restrict_range(start_after_cyclic, end_after_cyclic, restrict_start_point, restrict_end_point)
|
|
|
|
self.bones_points[obj_bonename][start_after_cyclic : end_after_cyclic] = cyclic_points[obj_bonename][start_after_cyclic : end_after_cyclic]
|
|
repeat_mp_difference(self, obj_bonename, repeat_after, difference[3:], length, start_p + 3, end_p, mod)
|
|
|
|
#Add the difference to cyclic after
|
|
if avz_mp.frame_start != self.avz_frame_start[obj_bonename]:
|
|
|
|
#Apply the restrict range
|
|
if mod.use_restricted_range:
|
|
start_before_cyclic, end_before_cyclic = restrict_range(start_before_cyclic, end_before_cyclic, restrict_start_point, restrict_end_point)
|
|
|
|
#Make sure the start index is not 0
|
|
start_before_cyclic = max(0, start_before_cyclic)
|
|
|
|
#Restore the original cyclic points before adding the difference
|
|
self.bones_points[obj_bonename][start_before_cyclic : end_before_cyclic] = cyclic_points[obj_bonename][start_before_cyclic : end_before_cyclic]
|
|
repeat_mp_difference(self, obj_bonename, repeat_before, difference[:-3], -length, start_p, end_p -3, mod)
|
|
|
|
frame_range_reset(self, context)
|
|
|
|
def get_mp_points(self, context, included = None):
|
|
'''Using Blender's motion path to get the points for the editable motion path'''
|
|
# print('get_mp_points')
|
|
#add a timer while recalculting the motion path to avoid issues with g hotkey
|
|
# self.timer_tick_counter = 0
|
|
# self.timer = context.window_manager.event_timer_add(0.1, window=context.window)
|
|
active_obj = context.view_layer.objects.active
|
|
#Check for the current selection and change to the relevant selection if necessery
|
|
#check this as a set since the bone order can change when another object is active
|
|
if context.selected_pose_bones and self.posemode:
|
|
selected_items = {(bone.id_data.name, bone.name) for bone in context.selected_pose_bones}
|
|
elif context.selected_objects and not self.posemode:
|
|
selected_items = {(obj.id_data.name, obj.name) for obj in context.selected_objects}
|
|
else:
|
|
selected_items = None
|
|
|
|
#if the bone selection is change then update the selection before running blender motion paths
|
|
if set(self.mp_bone_names) != selected_items:
|
|
if selected_items:
|
|
for obj_bonename in selected_items:
|
|
objname, bonename = obj_bonename
|
|
obj = context.view_layer.objects[objname]
|
|
context.view_layer.objects.active = obj
|
|
if self.posemode:
|
|
obj.data.bones[bonename].select = False
|
|
else:
|
|
obj.select_set(False)
|
|
|
|
for obj_bonename in self.mp_bone_names:
|
|
objname, bonename = obj_bonename
|
|
if included:
|
|
if obj_bonename not in included:
|
|
continue
|
|
obj = context.view_layer.objects[objname]
|
|
context.view_layer.objects.active = obj
|
|
if self.posemode:
|
|
obj.data.bones[bonename].select = True
|
|
else:
|
|
obj.select_set(True)
|
|
|
|
#Iterating first over the objects and then over the bones
|
|
for obj_bonename in self.mp_bone_names:
|
|
if included:
|
|
if obj_bonename not in included:
|
|
continue
|
|
objname, bonename = obj_bonename
|
|
obj = context.view_layer.objects[objname]
|
|
|
|
#blender also creates motion path per active object
|
|
context.view_layer.objects.active = obj
|
|
range_type = 'MANUAL' if bpy.app.version >= (4, 0, 0) else 'SCENE'
|
|
#Always using MANUAL because we are sampling every time separate chuncks
|
|
if self.posemode:
|
|
bpy.ops.pose.paths_calculate(range = range_type)
|
|
else:
|
|
bpy.ops.object.paths_calculate(range = range_type)
|
|
|
|
#Get all the point values from all the bones in the obj from Blender's motion path
|
|
for obj_bonename in self.mp_bone_names:
|
|
if included:
|
|
if obj_bonename not in included:
|
|
continue
|
|
objname, bonename = obj_bonename
|
|
obj = context.view_layer.objects[objname]
|
|
context.view_layer.objects.active = obj
|
|
if self.posemode and bonename not in obj.pose.bones:
|
|
continue
|
|
item = obj.pose.bones[bonename] if self.posemode else obj
|
|
if item.motion_path is None:
|
|
continue
|
|
length = len(item.motion_path.points)*3
|
|
points = np.zeros(length)
|
|
item.motion_path.points.foreach_get('co', points)
|
|
self.bones_points.update({obj_bonename : points})
|
|
|
|
#get the frames from the keyframes sorted
|
|
update_mp_bones_keys(self, item, obj_bonename)
|
|
|
|
|
|
if self.posemode:
|
|
bpy.ops.pose.paths_clear(only_selected=False)
|
|
else:
|
|
bpy.ops.object.paths_clear(only_selected=False)
|
|
|
|
# context.view_layer.objects.active = active_obj
|
|
#select back the bones
|
|
if set(self.mp_bone_names) != selected_items:
|
|
if selected_items:
|
|
for obj_bonename in selected_items:
|
|
objname, bonename = obj_bonename
|
|
obj = context.view_layer.objects[objname]
|
|
if self.posemode:
|
|
obj.data.bones[bonename].select = True
|
|
else:
|
|
obj.select_set(True)
|
|
|
|
for obj_bonename in self.mp_bone_names:
|
|
if selected_items and obj_bonename in selected_items:
|
|
continue
|
|
objname, bonename = obj_bonename
|
|
obj = context.view_layer.objects[objname]
|
|
# context.view_layer.objects.active = obj
|
|
if self.posemode:
|
|
obj.data.bones[bonename].select = False
|
|
else:
|
|
obj.select_set(False)
|
|
|
|
context.view_layer.objects.active = active_obj
|
|
#update the handles
|
|
get_handles(self)
|
|
|
|
def matrix_differece(self, context):
|
|
'''get the difference between the world matrix and the basis matrix in local space'''
|
|
self.matrix_diff = dict()
|
|
|
|
if self.posemode:
|
|
for bone in context.selected_pose_bones:
|
|
obj = bone.id_data
|
|
parent_matrix = bone.parent.matrix if bone.parent else Matrix.Identity(4)
|
|
|
|
#make sure the matrix is not zero to be able to invert it
|
|
matrix_basis_inverted = bone.matrix_basis.inverted() if bone.matrix_basis.determinant() else Matrix.Identity(4)
|
|
|
|
matrix_diff = obj.matrix_world @ bone.matrix @ matrix_basis_inverted @ parent_matrix.inverted()
|
|
self.matrix_diff.update({(bone.id_data.name, bone.name) : matrix_diff})
|
|
#to get the new world matrix use bone.parent.matrix @ matrix_diff @ bone.matrix_basis and then get the translation
|
|
else:
|
|
for obj in context.selected_objects:
|
|
parent_matrix = obj.parent.matrix_world if obj.parent else Matrix.Identity(4)
|
|
|
|
#make sure the matrix is not zero to be able to invert it
|
|
matrix_basis_inverted = obj.matrix_basis.inverted() if obj.matrix_basis.determinant() else Matrix.Identity(4)
|
|
|
|
matrix_diff = obj.matrix_world @ matrix_basis_inverted @ parent_matrix.inverted()
|
|
self.matrix_diff.update({(obj.id_data.name, obj.name) : matrix_diff})
|
|
|
|
|
|
def handles_selection_clear(self):
|
|
self.bones_selected_handles_l.clear()
|
|
self.bones_selected_handles_r.clear()
|
|
self.bones_handles_right.clear()
|
|
self.bones_handles_left.clear()
|
|
|
|
def align_handles(handle, other_handle, keyframe_coord, dist_len = None):
|
|
'''Keep handles aligned - flip the handle distance from keyframe and use lerp to keep length'''
|
|
other_handle_distance = ((other_handle - keyframe_coord) *-1)
|
|
handle_distance = (handle - keyframe_coord)
|
|
|
|
#use lerp to keep the handle length
|
|
factor = handle_distance.length /other_handle_distance.length
|
|
if dist_len:
|
|
factor *= dist_len
|
|
|
|
handle = keyframe_coord.lerp(keyframe_coord + other_handle_distance, factor)
|
|
|
|
return handle
|
|
|
|
def get_distance(key, self, context, bone, obj_bonename, frame, matrix_diff, coord_3d):
|
|
'''get the distance between the initial coordinates and the current one and convert to matrix distance'''
|
|
# parent_frame = None
|
|
|
|
initial_coord = self.initial_coord
|
|
if key == 'keyframe':
|
|
loc_coord = Vector(self.bones_keyframes_coords[obj_bonename][frame])
|
|
initial_coord = Vector(self.initial_keyframe_coords[obj_bonename][frame])
|
|
handle_frame = None
|
|
|
|
elif key == 'handle_r':
|
|
if frame not in self.bones_handles_right[obj_bonename]:
|
|
return
|
|
loc_coord = self.bones_handles_right[obj_bonename][frame]
|
|
if obj_bonename not in self.bones_selected_handles_r:
|
|
return False
|
|
if frame not in self.bones_selected_handles_r[obj_bonename]:
|
|
return False
|
|
handle_frame = self.bones_handles_frames[obj_bonename][frame][1]
|
|
|
|
elif key == 'handle_l':
|
|
if frame not in self.bones_handles_left[obj_bonename]:
|
|
return
|
|
loc_coord = self.bones_handles_left[obj_bonename][frame]
|
|
if obj_bonename not in self.bones_selected_handles_l:
|
|
return False
|
|
if frame not in self.bones_selected_handles_l[obj_bonename]:
|
|
return False
|
|
handle_frame = self.bones_handles_frames[obj_bonename][frame][0]
|
|
|
|
if bone.parent:
|
|
if handle_frame:
|
|
# handle_frame = add_frame_offset(bone.id_data, handle_frame, add = True)
|
|
if handle_frame != context.scene.frame_current_final:
|
|
handle_subframe, handle_frame = math.modf(handle_frame)
|
|
context.scene.frame_set(int(handle_frame), subframe = handle_subframe)
|
|
parent_matrix = bone.parent.matrix if self.posemode else bone.parent.matrix_world
|
|
else:
|
|
parent_matrix = Matrix.Identity(4)
|
|
|
|
distance = (parent_matrix.inverted() @ matrix_diff.inverted() @ loc_coord) - (parent_matrix.inverted() @ matrix_diff.inverted() @ initial_coord)
|
|
return distance
|
|
|
|
def update_mp_handle_types(self, context, handle_value):
|
|
'''update the values of the handles types from the motion path'''
|
|
if not self.bones_selected_keyframes:
|
|
return
|
|
update_frames = []
|
|
for obj_bonename, frames in self.bones_selected_keyframes.items():
|
|
objname, bonename = obj_bonename
|
|
obj = context.view_layer.objects[objname]
|
|
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
path = get_location_path(self.posemode, obj, bonename)
|
|
|
|
for i in range(3):
|
|
fcu = fcurves.find(data_path = path, index = i)
|
|
if fcu is None:
|
|
continue
|
|
for keyframe_point in fcu.keyframe_points:
|
|
frame = round(keyframe_point.co[0], 2)
|
|
frame = add_frame_offset(obj, frame)
|
|
if frame not in frames:
|
|
continue
|
|
handles = get_mp_handle_side(self, obj_bonename, frame)
|
|
|
|
if handle_value in {'LINEAR', 'BEZIER', 'CONSTANT'}:
|
|
#in this case handle_type is actually applying interpolation
|
|
keyframe_point.interpolation = handle_value
|
|
else:
|
|
if 'handle_r' in handles:
|
|
keyframe_point.handle_right_type = handle_value
|
|
if 'handle_l' in handles:
|
|
keyframe_point.handle_left_type = handle_value
|
|
|
|
update_frames.append(frame)
|
|
|
|
fcu.update()
|
|
return update_frames
|
|
|
|
def get_mp_handle_side(self, obj_bonename, frame):
|
|
'''In case of only one side is selected and need one handle type updated'''
|
|
handles = set()
|
|
if obj_bonename in self.bones_selected_handles_r:
|
|
if frame in self.bones_selected_handles_r[obj_bonename]:
|
|
handles.add('handle_r')
|
|
if obj_bonename in self.bones_selected_handles_l:
|
|
if frame in self.bones_selected_handles_l[obj_bonename]:
|
|
handles.add('handle_l')
|
|
|
|
if not handles:
|
|
handles = {'handle_r', 'handle_l'}
|
|
|
|
return handles
|
|
|
|
def mp_update_keyframes(self, context, obj_bonename, frame_distance, key):
|
|
'''updating the selected keyframes or handles after being moved and translated from world to local space'''
|
|
|
|
# for obj_bonename in self.bones_selected_keyframes.keys():
|
|
objname, bonename = obj_bonename
|
|
obj = context.view_layer.objects[objname]
|
|
distance = frame_distance
|
|
|
|
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
path = get_location_path(self.posemode, obj, bonename)
|
|
|
|
for i in range(3):
|
|
fcu = fcurves.find(data_path = path, index = i)
|
|
if fcu is None:
|
|
continue
|
|
keyframes = set()
|
|
for keyframe_point in fcu.keyframe_points:
|
|
# frame = round(keyframe_point.co[0], 2)
|
|
frame = keyframe_point.co[0]
|
|
frame = round(add_frame_offset(obj, frame, add = False), 2)
|
|
if frame not in self.bones_selected_keyframes[obj_bonename]:
|
|
continue
|
|
keyframes.add(frame)
|
|
#for scale calculation get the distance per frame
|
|
if self.scale or self.rotate:
|
|
distance = frame_distance[frame]
|
|
|
|
#update only the keyframes with handles
|
|
if key == 'keyframe':
|
|
keyframe_point.co[1] += distance[i]
|
|
keyframe_point.handle_right[1] += distance[i]
|
|
keyframe_point.handle_left[1] += distance[i]
|
|
|
|
elif distance: #update only the handles
|
|
#when using auto handles, update only from one side
|
|
side_auto = get_side_auto(self, obj_bonename, frame, key)
|
|
|
|
mp_update_keyframe_handles(self, obj_bonename, keyframe_point, frame, distance[i], 'handle_right', side_auto)
|
|
|
|
mp_update_keyframe_handles(self, obj_bonename, keyframe_point, frame, distance[i], 'handle_left', side_auto)
|
|
|
|
# add keyframes to channels that are missing a frame
|
|
missing_frames = set(self.bones_selected_keyframes[obj_bonename]).difference(keyframes)
|
|
if not missing_frames or not distance[i]:
|
|
continue
|
|
for missing_frame in missing_frames:
|
|
missing_frame = add_frame_offset(obj, missing_frame, add = True)
|
|
value = fcu.evaluate(missing_frame)
|
|
if key == 'keyframe':
|
|
value += distance[i]
|
|
keyframe_point = fcu.keyframe_points.insert(missing_frame, value)
|
|
|
|
# need to recalculate the handles and add their value
|
|
# if key != 'keyframe':
|
|
# get_handles(self)
|
|
# distance = get_distance(key, self, context, posebone, obj_bonename, frame, self.matrix_diff[obj_bonename], None)
|
|
|
|
def mp_update_keyframe_handles(self, obj_bonename, keyframe_point, frame, distance, handle, side_auto):
|
|
|
|
selection = self.bones_selected_handles_r if handle == 'handle_right' else self.bones_selected_handles_l
|
|
#getting the right side handles
|
|
if obj_bonename not in selection:
|
|
return
|
|
if side_auto not in {handle[:8], None}:
|
|
return
|
|
if frame not in selection[obj_bonename]:
|
|
return
|
|
keyframe_handle = getattr(keyframe_point, handle)
|
|
prev_handle = keyframe_handle.copy()
|
|
obj = bpy.context.view_layer.objects[obj_bonename[0]]
|
|
#getting the avg frame between all the handles
|
|
avg_frame = self.bones_handles_frames[obj_bonename][frame][1] if handle == 'handle_right' else self.bones_handles_frames[obj_bonename][frame][0]
|
|
avg_frame = add_frame_offset(obj, avg_frame, add = True) #reversing frame offset
|
|
|
|
factor = abs((avg_frame - keyframe_point.co[0]) / (keyframe_handle[0] - keyframe_point.co[0]))
|
|
|
|
#getting the value at avg
|
|
handle_avg_value = keyframe_point.co.lerp(keyframe_handle, factor)
|
|
handle_avg_value[1] += distance
|
|
#move back to the previous handle position
|
|
keyframe_handle = keyframe_point.co.lerp(handle_avg_value, 1/factor)
|
|
|
|
if self.bones_handles_types[obj_bonename][frame] == 'VECTOR':
|
|
keyframe_point.handle_left_type = 'FREE'
|
|
keyframe_point.handle_right_type = 'FREE'
|
|
self.bones_handles_types[obj_bonename][frame] = 'FREE'
|
|
setattr(keyframe_point, handle, keyframe_handle)
|
|
|
|
if self.bones_handles_types[obj_bonename][frame] == 'AUTO':
|
|
dist_len = abs((keyframe_handle - keyframe_point.co).length / (prev_handle - keyframe_point.co).length)
|
|
other_handle = 'handle_left' if handle == 'handle_right' else 'handle_right'
|
|
other_keyframe_handle = getattr(keyframe_point, other_handle)
|
|
|
|
other_new_value = align_handles(other_keyframe_handle, keyframe_handle, keyframe_point.co, dist_len)
|
|
setattr(keyframe_point, other_handle, other_new_value)
|
|
|
|
keyframe_point.handle_left_type = 'ALIGNED'
|
|
keyframe_point.handle_right_type = 'ALIGNED'
|
|
|
|
def get_side_auto(self, obj_bonename, frame, key):
|
|
'''In case of using automatic handles, and both sides are selected and being moved'''
|
|
if obj_bonename not in self.bones_handles_types:
|
|
return None
|
|
if frame not in self.bones_handles_types[obj_bonename]:
|
|
return None
|
|
if self.bones_handles_types[obj_bonename][frame] != 'AUTO':
|
|
return None
|
|
if obj_bonename in self.bones_selected_handles_r and obj_bonename in self.bones_selected_handles_l:
|
|
if frame in self.bones_selected_handles_r[obj_bonename] and frame in self.bones_selected_handles_l[obj_bonename]:
|
|
return key
|
|
|
|
return None
|
|
|
|
def handles_frame_remove(self, obj_bonename, frame):
|
|
handles = [self.bones_selected_handles_l, self.bones_selected_handles_r,
|
|
self.bones_handles_right, self.bones_handles_left]
|
|
for handle in handles:
|
|
if obj_bonename not in handle:
|
|
continue
|
|
#bones_selected_handles r using lists and bones_handles are using dict
|
|
if isinstance(handle[obj_bonename], list):
|
|
if frame in handle[obj_bonename]:
|
|
handle[obj_bonename].remove(frame)
|
|
elif isinstance(handle[obj_bonename], dict):
|
|
del handle[obj_bonename][frame]
|
|
if not len(handle[obj_bonename]):
|
|
del handle[obj_bonename]
|
|
|
|
def get_avg_handles(handle_side, keys, handles, obj):
|
|
'''Get the average frame and values of all the handles'''
|
|
#get the average of the frames from all the handles on all the location keyframes
|
|
handles_frame_avg = sum(handle[0] for handle in handles)/len(handles)
|
|
new_handles = []
|
|
for keyframe in keys:
|
|
if keyframe is None:
|
|
new_handles.append(None)
|
|
continue
|
|
handle = getattr(keyframe, handle_side)
|
|
try:
|
|
factor = (handles_frame_avg - keyframe.co[0]) / (handle[0] - keyframe.co[0])
|
|
except ZeroDivisionError:
|
|
factor = 1
|
|
new_value = keyframe.co.lerp(handle, factor)
|
|
new_handles.append(new_value[1])
|
|
# new_handles.append(handle[1])
|
|
|
|
return handles_frame_avg, new_handles
|
|
|
|
def get_handle_type(self, obj_bonename, frame, keys):
|
|
|
|
handles = []
|
|
for key in keys:
|
|
if key is None:
|
|
continue
|
|
handles.append(key.handle_left_type)
|
|
handles.append(key.handle_right_type)
|
|
|
|
handle_type = 'FREE' if any([handle in {'FREE', 'VECTOR'}for handle in handles]) else 'AUTO'
|
|
if obj_bonename in self.bones_handles_types:
|
|
# print('get handle type ', frame)
|
|
self.bones_handles_types[obj_bonename].update({frame: handle_type})
|
|
else:
|
|
self.bones_handles_types.update({obj_bonename : {frame : handle_type}})
|
|
|
|
def find_fcu_group(obj, path):
|
|
'''find the fcurve group '''
|
|
fcurves_container = Tools.get_fcurves_container(obj, obj.animation_data.action)
|
|
for group in fcurves_container.groups:
|
|
for channel in group.channels:
|
|
if path == channel.data_path:
|
|
return group
|
|
return None
|
|
|
|
def get_frame_keys(obj, fcurves, path, frames):
|
|
frame_keys = dict()
|
|
#iterate over the location arrays
|
|
for i in range(3):
|
|
fcu = fcurves.find(data_path = path, index = i)
|
|
if fcu is None:
|
|
continue
|
|
|
|
for keyframe_point in fcu.keyframe_points:
|
|
frame = keyframe_point.co[0]
|
|
frame = round(add_frame_offset(obj, frame), 2)
|
|
if frame not in frames:
|
|
continue
|
|
if keyframe_point.interpolation != 'BEZIER':
|
|
continue
|
|
if frame not in frame_keys:
|
|
#create an empty handle, in case one of the handles doesn't exist
|
|
frame_keys.update({frame : [None, None, None]})
|
|
frame_keys[frame][i] = keyframe_point
|
|
return frame_keys
|
|
|
|
def add_layer_to_handles(obj, path, frame, handle_right_values, handle_left_values):
|
|
if not obj.animation_data.use_nla:
|
|
return handle_right_values, handle_left_values
|
|
if not obj.animation_data.use_tweak_mode and obj.animation_data.action:
|
|
if obj.animation_data.action_influence == 1 and obj.animation_data.action_blend_type == 'REPLACE':
|
|
return handle_right_values, handle_left_values
|
|
|
|
blend_types = {'REPLACE' : '+', 'ADD' : '+', 'COMBINE' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
|
|
value_add = Vector((0, 0, 0))
|
|
handle_right_diff = Vector((0, 0, 0))
|
|
handle_left_diff = Vector((0, 0, 0))
|
|
|
|
for track in obj.animation_data.nla_tracks:
|
|
if track.mute:
|
|
continue
|
|
for strip in track.strips:
|
|
if strip.mute:
|
|
continue
|
|
fcurves = Tools.get_fcurves_channelbag(obj, strip.action)
|
|
for i in range(3):
|
|
fcu = fcurves.find(data_path = path, index = i)
|
|
if fcu is None:
|
|
continue
|
|
|
|
#get the difference value between the keyframe and the handle, and add the overall values to it
|
|
if strip.action == obj.animation_data.action:
|
|
handle_right_diff[i] = handle_right_values[i] - fcu.evaluate(frame)
|
|
handle_left_diff[i] = handle_left_values[i] - fcu.evaluate(frame)
|
|
|
|
value_add[i] = handles_add_to_value(value_add[i], fcu, frame, strip.influence, strip.blend_type, blend_types)
|
|
|
|
#In case not in tweak mode and there is an action on top of all the tracks
|
|
if not obj.animation_data.use_tweak_mode and obj.animation_data.action:
|
|
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
|
|
for i in range(3):
|
|
fcu = fcurves.find(data_path = path, index = i)
|
|
if fcu is None:
|
|
continue
|
|
#get the difference value between the keyframe and the handle, and add the overall values to it
|
|
handle_right_diff[i] = handle_right_values[i] - fcu.evaluate(frame)
|
|
handle_left_diff[i] = handle_left_values[i] - fcu.evaluate(frame)
|
|
value_add[i] = handles_add_to_value(value_add[i], fcu, frame, obj.animation_data.action_influence, obj.animation_data.action_blend_type, blend_types)
|
|
|
|
handle_right_values = handle_right_diff + value_add
|
|
handle_left_values = handle_left_diff + value_add
|
|
|
|
return handle_right_values, handle_left_values
|
|
|
|
def handles_add_to_value(value_add, fcu, frame, influence, blend_type, blend_types):
|
|
|
|
if blend_type =='REPLACE':
|
|
value_add = value_add * (1 - influence) + fcu.evaluate(frame) * influence
|
|
else:
|
|
value_add = eval('value_add' + blend_types[blend_type] +' fcu.evaluate(frame)' + '*' + str(influence))
|
|
|
|
return value_add
|
|
|
|
def get_handles(self):
|
|
'''Getting the coordinates and frames of the handles'''
|
|
if not self.bones_selected_keyframes:
|
|
return
|
|
if not bpy.context.scene.animtoolbox.mp_handles:
|
|
return
|
|
|
|
scene = bpy.context.scene
|
|
frame_current = scene.frame_current_final
|
|
for obj_bonename, frames in self.bones_selected_keyframes.items():
|
|
obj = bpy.context.view_layer.objects[obj_bonename[0]]
|
|
bone = obj.pose.bones[obj_bonename[1]] if self.posemode else obj
|
|
path = get_location_path(self.posemode, obj, obj_bonename[1])
|
|
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
|
|
#get all the keyframes and handles in a dictionary with rounded frames as the keys
|
|
frame_keys = get_frame_keys(obj, fcurves, path, frames)
|
|
|
|
#Get the average frame and then the value of the handles in this avg point
|
|
for frame, keys in frame_keys.items():
|
|
# frame = add_frame_offset(obj, frame)
|
|
handles_right = [keyframe.handle_right for keyframe in keys if keyframe is not None]
|
|
handles_left = [keyframe.handle_left for keyframe in keys if keyframe is not None]
|
|
|
|
handles_right_frame, handle_right_values = get_avg_handles('handle_right', keys, handles_right, obj)
|
|
handles_left_frame, handle_left_values = get_avg_handles('handle_left', keys, handles_left, obj)
|
|
handles_right_frame = add_frame_offset(obj, handles_right_frame, add = False)
|
|
handles_left_frame = add_frame_offset(obj, handles_left_frame, add = False)
|
|
|
|
get_handle_type(self, obj_bonename, frame, keys)
|
|
# to get the new world matrix use bone.parent.matrix @ matrix_diff @ bone.matrix_basis and then get the translation
|
|
scene.frame_set(int(handles_right_frame), subframe = handles_right_frame % 1)
|
|
bone_right_matrix = bone.matrix.copy() if self.posemode else bone.matrix_world.copy()
|
|
bone_right_matrix_basis = bone.matrix_basis.copy()
|
|
|
|
#if a keyframe is missing then get the array from the matrix basis
|
|
if None in keys:
|
|
handle_right_values = [bone.matrix_basis.translation[i] if key is None else handle_right_values[i] for i, key in enumerate(keys)]
|
|
|
|
scene.frame_set(int(handles_left_frame), subframe = handles_left_frame % 1)
|
|
bone_left_matrix = bone.matrix.copy() if self.posemode else bone.matrix_world.copy()
|
|
bone_left_matrix_basis = bone.matrix_basis.copy()
|
|
|
|
if None in keys:
|
|
handle_left_values = [bone.matrix_basis.translation[i] if key is None else handle_left_values[i] for i, key in enumerate(keys)]
|
|
|
|
handle_right_values, handle_left_values = add_layer_to_handles(obj, path, frame, handle_right_values, handle_left_values)
|
|
|
|
#converting the handle values to 3d coordinates
|
|
handle_right_coords = bone_right_matrix @ bone_right_matrix_basis.inverted() @ Vector(handle_right_values)
|
|
handle_left_coords = bone_left_matrix @ bone_left_matrix_basis.inverted() @ Vector(handle_left_values)
|
|
if self.posemode:
|
|
handle_right_coords = obj.matrix_world @ handle_right_coords
|
|
handle_left_coords = obj.matrix_world @ handle_left_coords
|
|
|
|
#update the coordinates of the handles
|
|
if obj_bonename in self.bones_handles_left:
|
|
|
|
self.bones_handles_right[obj_bonename].update({frame : handle_right_coords})
|
|
self.bones_handles_left[obj_bonename].update({frame : handle_left_coords})
|
|
self.bones_handles_frames[obj_bonename].update({frame : (handles_left_frame, handles_right_frame)})
|
|
#get the relation between the keyframe frames and the handle frames
|
|
else:
|
|
self.bones_handles_right.update({obj_bonename : {frame : handle_right_coords}})
|
|
self.bones_handles_left.update({obj_bonename : {frame : handle_left_coords}})
|
|
self.bones_handles_frames.update({obj_bonename : {frame : (handles_left_frame, handles_right_frame)}})
|
|
|
|
if scene.frame_current_final != frame_current:
|
|
scene.frame_set(int(frame_current), subframe = frame_current%1)
|
|
|
|
coords_2d_update(self, bpy.context)
|
|
return
|
|
|
|
def mp_handles_on_off(self, context):
|
|
'''Turn on or off motion path handles'''
|
|
|
|
if self.mp_handles == context.scene.animtoolbox.mp_handles:
|
|
return
|
|
|
|
if context.scene.animtoolbox.mp_handles:
|
|
get_handles(self)
|
|
else:
|
|
#reset all the handle values when turning off
|
|
self.bones_handles_right = dict()
|
|
self.bones_handles_left = dict()
|
|
self.bones_selected_handles_r = dict()
|
|
self.bones_selected_handles_l = dict()
|
|
self.bones_handles_frames = dict()
|
|
self.coords2d_handles_r = dict()
|
|
self.coords2d_handles_l = dict()
|
|
|
|
Tools.redraw_areas(['VIEW_3D'])
|
|
self.mp_handles = context.scene.animtoolbox.mp_handles
|
|
|
|
def compare_bone_keys(self,context, event):
|
|
'''check if the keyframe values were changed'''
|
|
|
|
if event.type not in {'INBETWEEN_MOUSEMOVE','MOUSEMOVE'}: #event.value != 'RELEASE' and
|
|
return False
|
|
global value_update
|
|
if not value_update:
|
|
return False
|
|
current_bones_keys = get_current_bones_keys(self)
|
|
if self.current_bones_keys == current_bones_keys:
|
|
return False
|
|
if not current_bones_keys:
|
|
return False
|
|
if not self.current_bones_keys:
|
|
return False
|
|
frames = []
|
|
skip = True
|
|
|
|
for obj_bonename, values in self.current_bones_keys.items():
|
|
if obj_bonename not in current_bones_keys:
|
|
continue
|
|
if not len(current_bones_keys[obj_bonename]) or not len(self.current_bones_keys[obj_bonename]):
|
|
continue
|
|
skip = False
|
|
|
|
#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 = []
|
|
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 = 1) - np.round(new_array[1], decimals = 1)
|
|
diff_frames = np.round(old_array[0], decimals = 1) - np.round(new_array[0], decimals = 1)
|
|
|
|
#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]])
|
|
|
|
if skip:
|
|
return False
|
|
|
|
if len(frames):
|
|
update_mp_points(self, context, frames, None, None)
|
|
else:
|
|
get_mp_points(self, context)
|
|
|
|
update_keyframe_coords(self)
|
|
coords_2d_update(self, context)
|
|
self.current_bones_keys = current_bones_keys
|
|
|
|
#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.animtoolbox.selected_keyframes = serialize_dict(self.bones_selected_keyframes)
|
|
|
|
return True
|
|
|
|
def compare_obj_layers(self, context, event):
|
|
'''redraw motion path if layer properties changed'''
|
|
obj = context.object
|
|
if not obj.animation_data:
|
|
return
|
|
if not obj.animation_data.use_nla:
|
|
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
|
|
global value_update
|
|
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()
|
|
value_update = True
|
|
if not value_update:
|
|
return
|
|
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()
|
|
get_mp_points(self, context)
|
|
update_frame_offsets(self, context, old_bones_keys)
|
|
get_handles(self)
|
|
coords_2d_update(self, context)
|
|
|
|
self.obj_layer_properties = obj_layer_properties
|
|
value_update = False
|
|
|
|
def update_frame_offsets(self, context, old_bones_keys):
|
|
'''update all the frame offsets'''
|
|
|
|
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]
|
|
layer_properties.append(layer.frame_range)
|
|
|
|
return layer_properties
|
|
|
|
def update_keyframe_coords(self):
|
|
self.bones_keyframes_coords = dict()
|
|
|
|
for obj_bonename, points in self.bones_points.items():
|
|
keyframes = self.mp_bones_keys[obj_bonename]
|
|
keyframes_coords = dict()
|
|
|
|
for i in range(0, len(points), 3):
|
|
coords = tuple(points[i : i+3])
|
|
frame = ((i+3) / 3) + self.avz_frame_start[obj_bonename] - 1
|
|
frame = add_frame_offset(bpy.data.objects[obj_bonename[0]], frame, add = True)
|
|
|
|
if frame not in keyframes:
|
|
continue
|
|
keyframes_coords.update({frame : coords})
|
|
self.bones_keyframes_coords.update({obj_bonename : keyframes_coords})
|
|
|
|
def get_mp_items(self):
|
|
'''Get the objects or bones that are included in the motion path
|
|
since it's getting broken when using undo'''
|
|
if self.posemode:
|
|
self.items = [bpy.data.objects[objname].pose.bones[bonename] for objname, bonename in self.mp_bone_names]
|
|
else:
|
|
self.items = [bpy.data.objects[objname] for objname, bonename in self.mp_bone_names]
|
|
|
|
def get_current_bones_keys(self):
|
|
'''create the dictionary from the current selected bones keyframes values to compare and check if they changed.
|
|
the keyframes include all channels and bezier handle values'''
|
|
#if we are currently moving the selection with motion path, then we just need to check the location keyframes
|
|
current_selected_items = bpy.context.selected_pose_bones if self.posemode else bpy.context.selected_objects
|
|
if not current_selected_items:
|
|
return None
|
|
|
|
current_bones_keys = dict()
|
|
transforms = ['location', 'rotation', 'scale']
|
|
|
|
if all(selection in self.items for selection in current_selected_items):
|
|
transforms = ['location']
|
|
items = self.items
|
|
else:
|
|
items = self.current_selected_items
|
|
|
|
key_props = ['co', 'handle_left','handle_right']# 'handle_left_type', 'handle_right_type' , 'interpolation'
|
|
|
|
for item in items:
|
|
# Replace the rotation path with the actual rotation of the selection
|
|
if 'rotation' in transforms:
|
|
transforms[1] = 'rotation_euler' if len(item.rotation_mode) == 3 else 'rotation_' + item.rotation_mode.lower()
|
|
|
|
#create the dictionary of the bones and their keyframes
|
|
obj_bonename = (item.id_data.name, item.name)
|
|
current_bones_keys.update({obj_bonename : []})
|
|
#add all the keyframes and interpolations
|
|
for transform in transforms:
|
|
for prop in key_props:
|
|
current_bones_keys[obj_bonename] = list(get_bone_keyframes(item, transform, prop, current_bones_keys[obj_bonename]))
|
|
|
|
return current_bones_keys
|
|
|
|
def get_bone_keyframes(bone, transform = 'location', property = 'co', bones_keys = []):
|
|
'''get the keyframes of the selected bones or objects'''
|
|
|
|
if transform == 'rotation_quaternion' or transform == 'rotation_axis_angle':
|
|
array_len = 4
|
|
else:
|
|
array_len = 3
|
|
|
|
#get the keyframes
|
|
bone_path = bone.path_from_id() + '.' if type(bone) == bpy.types.PoseBone else ''
|
|
|
|
obj = bone.id_data
|
|
|
|
if obj.animation_data is None:
|
|
return []
|
|
if obj.animation_data.action is None:
|
|
return []
|
|
|
|
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
|
|
for i in range(array_len):
|
|
fcu = fcurves.find(data_path = bone_path + transform, index = i)
|
|
if fcu is None:
|
|
continue
|
|
length = len(fcu.keyframe_points) * 2
|
|
|
|
if property != 'interpolation':
|
|
keyframes = np.zeros(length)
|
|
# keyframes = [None]*length
|
|
fcu.keyframe_points.foreach_get(property, keyframes)
|
|
else:
|
|
keyframes = []
|
|
for keyframe in fcu.keyframe_points:
|
|
keyframes.append(keyframe.interpolation)
|
|
keyframes.append(keyframe.handle_left_type)
|
|
keyframes.append(keyframe.handle_right_type)
|
|
|
|
bones_keys = np.append(bones_keys, keyframes)
|
|
|
|
return bones_keys
|
|
|
|
def check_bone_names(self, context):
|
|
#check if the object or bone name was changed
|
|
global value_update
|
|
if not value_update:
|
|
return
|
|
changed = False
|
|
for i, obj_bonename in enumerate(self.mp_bone_names):
|
|
obj_name, bonename = obj_bonename
|
|
#find the object name if it was changed
|
|
if obj_name not in context.view_layer.objects:
|
|
for obj in context.view_layer.objects:
|
|
if 'atb_mp_name' not in obj.keys():
|
|
continue
|
|
if obj['atb_mp_name'] == obj_name:
|
|
self.mp_bone_names[i] = (obj.name, bonename)
|
|
update_dictionaries_keys(self, (obj.name, bonename), obj_bonename)
|
|
obj['atb_mp_name'] = obj.name
|
|
break
|
|
changed = True
|
|
else:
|
|
obj = context.view_layer.objects[obj_name]
|
|
|
|
if obj is None:
|
|
continue
|
|
|
|
if not self.posemode:
|
|
continue
|
|
|
|
#check if bone names were changed
|
|
if bonename not in obj.pose.bones:
|
|
for bone in obj.pose.bones:
|
|
if 'atb_mp_name' not in bone.keys():
|
|
continue
|
|
if bone['atb_mp_name'] == bonename:
|
|
#if it found the old name in the property then update all the attributes with the new name
|
|
self.mp_bone_names[i] = (obj.name, bone.name)
|
|
update_dictionaries_keys(self, (obj.name, bone.name), obj_bonename)
|
|
bone['atb_mp_name'] = bone.name
|
|
break
|
|
changed = True
|
|
|
|
return changed
|
|
|
|
def update_dictionaries_keys(self, new_names, old_names):
|
|
'''update all the attributes with the new object and bone names, replacing the dict keys but keeping the values
|
|
using this when name of objects or bones was changed'''
|
|
|
|
dicts_to_update = [self.bones_points, self.mp_bones_keys, self.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]
|
|
|
|
# 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)
|
|
|
|
|
|
def mp_frame_range_update(self, context):
|
|
'''updating the frame range with the properties - currently not used. might be removed'''
|
|
|
|
#Turn off markers range when changing frame range type
|
|
if self.mp_frame_range != 'MANUAL' and context.scene.animtoolbox.bake_frame_range:
|
|
context.scene.animtoolbox.bake_frame_range = False
|
|
|
|
if self.bake_frame_end:
|
|
return
|
|
|
|
self.bake_frame_end = context.scene.frame_end
|
|
self.bake_frame_start = context.scene.frame_start
|
|
|
|
########################################################################################################################
|
|
|
|
classes = (MotionPathOperator)
|
|
|
|
def register():
|
|
from bpy.utils import register_class
|
|
register_class(MotionPathOperator)
|
|
# for cls in classes:
|
|
# register_class(cls)
|
|
|
|
def unregister():
|
|
from bpy.utils import unregister_class
|
|
unregister_class(MotionPathOperator)
|
|
# for cls in classes:
|
|
# unregister_class(cls)
|