2966 lines
120 KiB
Python
2966 lines
120 KiB
Python
import bpy
|
|
import blf
|
|
import numpy as np
|
|
import mathutils
|
|
import bisect
|
|
from mathutils import Matrix
|
|
from mathutils import Vector
|
|
# import math
|
|
from math import pi
|
|
from . import emp
|
|
# import time
|
|
from bpy.app.handlers import persistent
|
|
from bpy_extras import anim_utils
|
|
|
|
# import gpu
|
|
from gpu_extras.batch import batch_for_shader
|
|
# import bpy_extras
|
|
#from bpy_extras.view3d_utils import location_3d_to_region_2d
|
|
|
|
def sort_selection(new_selection, selected_items, active_item):
|
|
|
|
if not selected_items:
|
|
return new_selection
|
|
|
|
if len(new_selection) < len(selected_items):
|
|
new_bones = set(selected_items) - set(new_selection)
|
|
new_selection += list(new_bones)
|
|
elif len(new_selection) > len(selected_items):
|
|
new_bones = set(new_selection) - set(selected_items)
|
|
for bone in new_bones:
|
|
new_selection.remove(bone)
|
|
elif set(new_selection) != set(selected_items):
|
|
new_selection = selected_items
|
|
|
|
if active_item in new_selection[:-1]:
|
|
new_selection.remove(active_item)
|
|
new_selection.append(active_item)
|
|
|
|
return new_selection
|
|
|
|
@persistent
|
|
def selection_order(self, context):
|
|
|
|
global bone_selection, obj_selection
|
|
|
|
if 'bone_selection' not in globals():
|
|
bone_selection = []
|
|
if 'obj_selection' not in globals():
|
|
obj_selection = []
|
|
|
|
selected_pose_bones = bpy.context.selected_pose_bones
|
|
selected_objects = bpy.context.selected_objects
|
|
|
|
bone_selection = sort_selection(bone_selection, selected_pose_bones, bpy.context.active_pose_bone)
|
|
obj_selection = sort_selection(obj_selection, selected_objects, bpy.context.active_object)
|
|
|
|
def asset_shelf_height(context):
|
|
'''Get the height of the asset shelf to always draw notification on top of it'''
|
|
if context.area.type != 'VIEW_3D':
|
|
return 0
|
|
if context.area.regions[2].type != 'ASSET_SHELF':
|
|
return 0
|
|
as_height = context.area.regions[2].height
|
|
return as_height
|
|
|
|
def draw_text_callback_px(self, context):
|
|
'''Write a text notification during modal operators'''
|
|
font_id = 0 # XXX, need to find out how best to get this.
|
|
#fade in
|
|
factor = 0.5
|
|
if not self.fade_out_start:
|
|
alpha = (self.timer.time_duration / self.fade_duration) * factor if not self.no_timer else factor
|
|
else:
|
|
timer = self.timer.time_duration - self.fade_out_start
|
|
alpha = (1 * factor) - ((timer / self.fade_duration ) * factor)
|
|
|
|
as_height = asset_shelf_height(context)
|
|
# draw some text
|
|
blf.position(font_id, 70, 45 + as_height, 0)
|
|
blf.size(font_id, self.size)
|
|
blf.color(0, 1, 1, 0, alpha)
|
|
blf.draw(font_id, self.text)
|
|
|
|
class Markers_Retimer(bpy.types.Operator):
|
|
"""Use Markers to retime your keyframes"""
|
|
bl_idname = "anim.markers_retimer"
|
|
bl_label = "Markers_Retimer"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return len(context.scene.timeline_markers) > 1
|
|
|
|
def store_markers(self, scene):
|
|
self.marker_frames = {}
|
|
for marker in scene.timeline_markers:
|
|
self.marker_frames.update({marker : marker.frame})
|
|
#create a list of the frames sorted
|
|
self.sorted_frames = sorted(self.marker_frames.values())
|
|
|
|
def invoke(self, context, event):
|
|
#If the modal is already running, then don't run it the second time
|
|
scene = context.scene
|
|
ui = context.window_manager.atb_ui
|
|
|
|
if ui.markers_retimer:
|
|
ui.markers_retimer = False
|
|
return {'CANCELLED'}
|
|
|
|
self.store_markers(scene)
|
|
ui.markers_retimer = True
|
|
|
|
if context.area.type == 'VIEW_3D':
|
|
# the arguments we pass the the callback
|
|
args = (self, context)
|
|
# Add the region OpenGL drawing callback
|
|
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
|
|
wm = context.window_manager
|
|
self.timer = wm.event_timer_add(0.1, window=context.window)
|
|
self.no_timer = False
|
|
self.fade_out_start = False
|
|
self.fade_duration = 3.0
|
|
self.text = 'Markers Retimer is On'
|
|
self.size = 20
|
|
self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(draw_text_callback_px, args, 'WINDOW', 'POST_PIXEL')
|
|
bpy.app.driver_namespace['markers_retimer_dh'] = self.draw_handle
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
try:
|
|
scene = context.scene
|
|
ui = context.window_manager.atb_ui
|
|
|
|
if event.type in {'ESC'}:
|
|
ui.markers_retimer = False
|
|
|
|
if not ui.markers_retimer:
|
|
bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW')
|
|
if 'markers_retimer_dh' in bpy.app.driver_namespace:
|
|
bpy.app.driver_namespace.pop('markers_retimer_dh')
|
|
redraw_areas(['VIEW_3D'])
|
|
self.report({'INFO'},'Quitting RetimerMarkers')
|
|
context.window_manager.event_timer_remove(self.timer)
|
|
return {'FINISHED'}
|
|
|
|
if event.type == 'TIMER':
|
|
#check if the fade in finished and remove the timer
|
|
if self.timer.time_duration >= self.fade_duration and not self.no_timer:
|
|
# context.window_manager.event_timer_remove(self.timer)
|
|
self.no_timer = True
|
|
else:
|
|
redraw_areas(['VIEW_3D'])
|
|
|
|
#continue if nothing changed
|
|
marker_frames = np.array(list(self.marker_frames.values()))
|
|
markers_array = np.zeros(len(scene.timeline_markers))
|
|
scene.timeline_markers.foreach_get('frame', markers_array)
|
|
if np.array_equal(marker_frames, markers_array):
|
|
return{'PASS_THROUGH'}
|
|
|
|
|
|
# context.area.tag_redraw()
|
|
#if markers were added or removed then restore them
|
|
if len(scene.timeline_markers) != len(self.marker_frames):
|
|
self.store_markers(scene)
|
|
return{'PASS_THROUGH'}
|
|
|
|
#If markers are the same then skip
|
|
marker_frames = np.array(list(self.marker_frames.values()))
|
|
markers_array = np.zeros(len(scene.timeline_markers))
|
|
scene.timeline_markers.foreach_get('frame', markers_array)
|
|
if np.array_equal(marker_frames, markers_array):
|
|
return{'PASS_THROUGH'}
|
|
|
|
# actions = set()
|
|
all_fcurves = set()
|
|
#get all the actions that are going to be influenced from all objects and layers
|
|
for obj in context.selected_objects:
|
|
if obj.animation_data is None:
|
|
continue
|
|
if obj.animation_data.use_nla:
|
|
for track in obj.animation_data.nla_tracks:
|
|
if track.mute:
|
|
continue
|
|
for strip in track.strips:
|
|
if strip.mute:
|
|
continue
|
|
# actions.add(strip.action)
|
|
all_fcurves.add(get_fcurves_channelbag(obj, strip.action))
|
|
fcurves = get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
if fcurves not in all_fcurves:
|
|
all_fcurves.add(fcurves)
|
|
# actions.add(obj.animation_data.action)
|
|
|
|
prev_frames = {}
|
|
next_frames = {}
|
|
changed_markers = {}
|
|
prev_dist_dict = {}
|
|
next_dist_dict = {}
|
|
|
|
#check which marker was changed
|
|
for marker in scene.timeline_markers:
|
|
#If markers are not stored anymore in case of undo, store them again and start again
|
|
if marker not in self.marker_frames:
|
|
self.store_markers(scene)
|
|
return {'PASS_THROUGH'}
|
|
|
|
#check if the marker frame was changed and if yes then calculate the percentage of the scale
|
|
if marker.frame != self.marker_frames[marker]:
|
|
#start_time = time.time()
|
|
|
|
old_frame = self.marker_frames[marker]
|
|
frame_index = self.sorted_frames.index(self.marker_frames[marker])
|
|
#get the previous frame if it's not the first marker frame
|
|
if frame_index:
|
|
previous_frame = self.sorted_frames[frame_index-1]
|
|
old_prev_dist = old_frame - previous_frame
|
|
new_prev_dist = marker.frame - previous_frame
|
|
prev_dist_perc = (new_prev_dist - old_prev_dist) / old_prev_dist
|
|
|
|
prev_frames.update({old_frame : previous_frame})
|
|
prev_dist_dict.update({old_frame : prev_dist_perc})
|
|
|
|
#get the next frame if it's not the last marker frame
|
|
if max(self.sorted_frames) != old_frame:
|
|
next_frame = self.sorted_frames[frame_index+1]
|
|
old_next_dist = next_frame - old_frame
|
|
new_next_dist = next_frame - marker.frame
|
|
next_dist_perc = (new_next_dist - old_next_dist) / old_next_dist
|
|
|
|
next_frames.update({old_frame : next_frame})
|
|
next_dist_dict.update({old_frame : next_dist_perc})
|
|
|
|
changed_markers.update({old_frame : marker})
|
|
|
|
#adding the distance calculation to the keyframes
|
|
for fcurves in all_fcurves:
|
|
for fcu in fcurves:
|
|
if filter_properties(context.scene.animtoolbox, fcu):
|
|
continue
|
|
keyframes = np.zeros([len(fcu.keyframe_points)*2])
|
|
updated_left_handle = np.zeros([len(fcu.keyframe_points)*2])
|
|
updated_right_handle = np.zeros([len(fcu.keyframe_points)*2])
|
|
fcu.keyframe_points.foreach_get('co', keyframes)
|
|
fcu.keyframe_points.foreach_get('handle_left', updated_left_handle)
|
|
fcu.keyframe_points.foreach_get('handle_right', updated_right_handle)
|
|
#initializing a new array to add
|
|
added_frames = np.zeros([len(fcu.keyframe_points)*2])
|
|
|
|
if bpy.context.scene.animtoolbox.filter_keyframes:
|
|
select_control_points = np.zeros([len(fcu.keyframe_points)])
|
|
fcu.keyframe_points.foreach_get('select_control_point', select_control_points)
|
|
else:
|
|
select_control_points = 1
|
|
|
|
#find the begining of the array to start iterating
|
|
start_index = 0
|
|
end_index = len(keyframes)
|
|
|
|
#get the start frame, the one before the first changed marker
|
|
if prev_frames and len(prev_frames) == len(changed_markers):
|
|
previous_frame = prev_frames[min(changed_markers.keys())]
|
|
indices = np.where(keyframes[::2] > previous_frame)[0] * 2
|
|
if len(indices):
|
|
start_index = indices[0]
|
|
|
|
#get the last frame, the one before the first changed marker
|
|
if next_frames and len(next_frames) == len(changed_markers):
|
|
next_frame = next_frames[max(changed_markers.keys())]
|
|
indices = np.where(keyframes[::2] > next_frame)[0] * 2
|
|
if len(indices):
|
|
end_index = indices[0]
|
|
|
|
for i in range(start_index, end_index, 2):
|
|
#get the keyframe position along all the markers
|
|
key_in_markers = list(marker_frames)
|
|
if keyframes[i] not in changed_markers:
|
|
key_in_markers.append(keyframes[i])
|
|
key_in_markers.sort()
|
|
else:
|
|
#in case the keyframe is exactly on the frame
|
|
old_frame = keyframes[i]
|
|
marker = changed_markers[keyframes[i]]
|
|
added_frames[i]= marker.frame - old_frame
|
|
continue
|
|
|
|
marker_i = key_in_markers.index(keyframes[i])
|
|
prev_marker = None if marker_i - 1 < 0 else key_in_markers[marker_i - 1]
|
|
next_marker = None if marker_i + 1 > len(key_in_markers)-1 else key_in_markers[marker_i + 1]
|
|
|
|
#markers don't affect this frame
|
|
if prev_marker not in changed_markers and next_marker not in changed_markers:
|
|
continue
|
|
|
|
#add from both directions
|
|
if (next_marker in changed_markers and prev_marker in changed_markers) or (next_marker in changed_markers and prev_marker is None):
|
|
# if both are selected or it the next is the first marker
|
|
old_frame = next_marker
|
|
marker = changed_markers[old_frame]
|
|
added_frames[i]= marker.frame - old_frame
|
|
continue
|
|
|
|
if prev_marker in changed_markers:
|
|
if next_marker is None:
|
|
old_frame = prev_marker
|
|
marker = changed_markers[old_frame]
|
|
added_frames[i]= marker.frame - old_frame
|
|
else:
|
|
frame_dist = keyframes[i] - next_marker
|
|
next_dist_perc = next_dist_dict[prev_marker]
|
|
added_frames[i]= (frame_dist * next_dist_perc)
|
|
|
|
elif next_marker in changed_markers:
|
|
#I needed to flip here next marker with prev distance
|
|
frame_dist = keyframes[i] - prev_marker
|
|
prev_dist_perc = prev_dist_dict[next_marker]
|
|
added_frames[i]= (frame_dist * prev_dist_perc)
|
|
|
|
#add only to selected keyframes
|
|
added_frames[::2] *= select_control_points
|
|
#add the whole array to the keyframes
|
|
keyframes += added_frames
|
|
updated_left_handle += added_frames
|
|
updated_right_handle += added_frames
|
|
|
|
fcu.keyframe_points.foreach_set('co', keyframes)
|
|
fcu.keyframe_points.foreach_set('handle_left', updated_left_handle)
|
|
fcu.keyframe_points.foreach_set('handle_right', updated_right_handle)
|
|
fcu.update()
|
|
|
|
# self.sorted_frames[frame_index] = marker.frame
|
|
# self.marker_frames[marker] = marker.frame
|
|
|
|
#end_time= time.time()
|
|
# break
|
|
|
|
redraw_areas(['DOPESHEET_EDITOR', 'GRAPH_EDITOR'])
|
|
self.store_markers(scene)
|
|
return {'PASS_THROUGH'}
|
|
|
|
except Exception as e:
|
|
# Log the error
|
|
bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW')
|
|
if 'markers_retimer_dh' in bpy.app.driver_namespace:
|
|
bpy.app.driver_namespace.pop('markers_retimer_dh')
|
|
redraw_areas(['VIEW_3D'])
|
|
context.window_manager.event_timer_remove(self.timer)
|
|
|
|
print("Error:", e)
|
|
self.report({'ERROR'}, str(e) + '. Quitting RetimerMarkers')
|
|
return {'CANCELLED'}
|
|
|
|
def init_frame_start_marker(self, scene, name = "Frame_Start"):
|
|
'''Create the start frame marker'''
|
|
self.frame_start_marker = scene.timeline_markers.new(name, frame = self.frame_start)
|
|
self.frame_start_marker[ "Frame_Start"] = True
|
|
self.frame_start_marker.select = False
|
|
self.frame_start_marker_name = name
|
|
|
|
def init_frame_end_marker(self, scene, name = "Frame_End"):
|
|
'''Create the start frame marker'''
|
|
self.frame_end_marker = scene.timeline_markers.new(name, frame = self.frame_end)
|
|
self.frame_end_marker["Frame_End"] = True
|
|
self.frame_end_marker.select = False
|
|
self.frame_end_marker_name = name
|
|
|
|
def check_removed_markers(self, scene):
|
|
#check if markers are gone in case of undo or if removed
|
|
|
|
markers = [marker for marker in scene.timeline_markers]
|
|
if self.frame_end_marker not in markers or self.frame_start_marker not in markers:
|
|
start_marker = next((marker for marker in scene.timeline_markers if marker.name == self.frame_start_marker_name), None) #and 'Frame_Start' in marker
|
|
end_marker = next((marker for marker in scene.timeline_markers if marker.name == self.frame_end_marker_name), None) # and 'Frame_End' in marker
|
|
if start_marker is not None:
|
|
self.frame_start_marker = start_marker
|
|
else:
|
|
init_frame_start_marker(self, scene, self.frame_start_marker_name)
|
|
if end_marker is not None:
|
|
self.frame_end_marker = end_marker
|
|
else:
|
|
init_frame_end_marker(self, scene, self.frame_end_marker_name)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def update_frame_range_markers(self, scene, frame_start, frame_end):
|
|
#update the frame range using marker only if it was changed both from the scene frame range and the operator frame range properties
|
|
if getattr(scene, frame_start) != self.frame_start_marker.frame and self.frame_start_marker.frame != self.frame_start:
|
|
if self.frame_start_marker.frame < 0:
|
|
self.frame_start_marker.frame = 0
|
|
setattr(scene, frame_start, self.frame_start_marker.frame)
|
|
self.frame_start = self.frame_start_marker.frame
|
|
redraw_areas(['VIEW_3D'])
|
|
return True
|
|
elif getattr(scene, frame_start) != self.frame_start:
|
|
self.frame_start_marker.frame = self.frame_start = getattr(scene, frame_start)
|
|
return True
|
|
|
|
if getattr(scene, frame_end) != self.frame_end_marker.frame and self.frame_end_marker.frame != self.frame_end:
|
|
setattr(scene, frame_end, self.frame_end_marker.frame)
|
|
self.frame_end = self.frame_end_marker.frame
|
|
redraw_areas(['VIEW_3D'])
|
|
return True
|
|
elif getattr(scene, frame_end) != self.frame_end:
|
|
self.frame_end_marker.frame = self.frame_end = getattr(scene, frame_end)
|
|
|
|
return False
|
|
|
|
class Markers_FrameRange(bpy.types.Operator):
|
|
"""Create Markers for an interactive frame range"""
|
|
bl_idname = "anim.markers_framerange"
|
|
bl_label = "Markers_Framerange"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
frame_start: bpy.props.IntProperty(name = 'Frame Start', description="Storing the frame start to check if the property or marker were changed")
|
|
frame_end: bpy.props.IntProperty(name = 'Frame End', description="Storing the frame end to check if the property or marker were changed")
|
|
|
|
def execute(self, context):
|
|
context.scene.animtoolbox.marker_frame_range = False
|
|
context.scene.timeline_markers.remove(self.frame_start_marker)
|
|
context.scene.timeline_markers.remove(self.frame_end_marker)
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
#If the modal is already running, then don't run it the second time
|
|
scene = context.scene
|
|
atb = scene.animtoolbox
|
|
if atb.marker_frame_range:
|
|
atb.marker_frame_range = False
|
|
return {'CANCELLED'}
|
|
|
|
scene.animtoolbox.marker_frame_range = True
|
|
|
|
self.frame_start = scene.frame_preview_start if scene.use_preview_range else scene.frame_start
|
|
self.frame_end = scene.frame_preview_end if scene.use_preview_range else scene.frame_end
|
|
init_frame_start_marker(self, scene)
|
|
init_frame_end_marker(self, scene)
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
scene = context.scene
|
|
|
|
if check_removed_markers(self, scene):
|
|
return {'PASS_THROUGH'}
|
|
|
|
# #If one of the markers were removed then remove the rest and quit the modal operator
|
|
# if self.frame_start_marker.name not in scene.timeline_markers:
|
|
# if self.frame_end_marker.name in scene.timeline_markers:
|
|
# scene.timeline_markers.remove(self.frame_end_marker)
|
|
# context.scene.marker_frame_range = False
|
|
# return {'CANCELLED'}
|
|
# if self.frame_end_marker.name not in scene.timeline_markers:
|
|
# if self.frame_start_marker.name in scene.timeline_markers:
|
|
# scene.timeline_markers.remove(self.frame_start_marker)
|
|
# context.scene.marker_frame_range = False
|
|
# return {'CANCELLED'}
|
|
|
|
#If the modal was running again or changed then exit
|
|
if not scene.animtoolbox.marker_frame_range:
|
|
self.execute(context)
|
|
return {'FINISHED'}
|
|
|
|
#get the attribute depending if using preview range or normal frame range
|
|
if scene.use_preview_range:
|
|
frame_start = 'frame_preview_start'
|
|
frame_end = 'frame_preview_end'
|
|
else:
|
|
frame_start = 'frame_start'
|
|
frame_end = 'frame_end'
|
|
|
|
# frame_start = getattr(scene, frame_start_attr)
|
|
# frame_end = getattr(scene, frame_end_attr)
|
|
|
|
if update_frame_range_markers(self, scene, frame_start, frame_end):
|
|
return {'RUNNING_MODAL'}
|
|
|
|
if event.type in {'ESC'}: # Cancel
|
|
self.execute(context)
|
|
return {'FINISHED'}
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
class Markers_BakeRange(bpy.types.Operator):
|
|
"""Create Markers for an interactive bake frame range"""
|
|
bl_idname = "anim.markers_bakerange"
|
|
bl_label = "Markers_Bakerange"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
frame_start: bpy.props.IntProperty(name = 'Frame Start', description="Storing the frame start to check if the property or marker were changed")
|
|
frame_end: bpy.props.IntProperty(name = 'Frame End', description="Storing the frame end to check if the property or marker were changed")
|
|
|
|
def execute(self, context):
|
|
context.scene.animtoolbox.bake_frame_range = False
|
|
context.scene.timeline_markers.remove(self.frame_start_marker)
|
|
context.scene.timeline_markers.remove(self.frame_end_marker)
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
#If the modal is already running, then don't run it the second time
|
|
scene = context.scene
|
|
atb = scene.animtoolbox
|
|
if atb.bake_frame_range:
|
|
atb.bake_frame_range = False
|
|
return {'CANCELLED'}
|
|
|
|
scene.animtoolbox.bake_frame_range = True
|
|
|
|
self.frame_start = atb.bake_frame_start
|
|
self.frame_end = atb.bake_frame_end
|
|
#init_frame_range_markers(self, scene)
|
|
init_frame_start_marker(self, scene, name = 'Bake Start')
|
|
init_frame_end_marker(self, scene, name = 'Bake End')
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
scene = context.scene
|
|
|
|
if check_removed_markers(self, scene):
|
|
return {'PASS_THROUGH'}
|
|
|
|
#If the modal was running again or changed then exit
|
|
if not scene.animtoolbox.bake_frame_range:
|
|
self.execute(context)
|
|
return {'FINISHED'}
|
|
|
|
#get the attribute depending if using preview range or normal frame range
|
|
frame_start = 'bake_frame_start'
|
|
frame_end = 'bake_frame_end'
|
|
|
|
if update_frame_range_markers(self, scene.animtoolbox, frame_start, frame_end):
|
|
return {'RUNNING_MODAL'}
|
|
|
|
if event.type in {'ESC'}: # Cancel
|
|
self.execute(context)
|
|
return {'FINISHED'}
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
def relative_cursor_frame(self, context):
|
|
'''updating the cursor matrix after every frame change'''
|
|
context = bpy.context
|
|
obj = context.object
|
|
if context.selected_pose_bones:
|
|
selected_items = context.selected_pose_bones
|
|
attr = 'matrix'
|
|
else:
|
|
selected_items = context.selected_objects
|
|
attr = 'matrix_world'
|
|
update_cursor_matrix(context)
|
|
global last_matrices
|
|
last_matrices = [getattr(item, attr).copy() for item in selected_items]
|
|
|
|
def cursor_change_post(self, context):
|
|
'''Depsgraph Handler to update cursor matrix if it was moved'''
|
|
global cursor_matrix, last_matrices
|
|
context = bpy.context
|
|
|
|
if context.selected_pose_bones:
|
|
new_matrices = [bone.matrix.copy() for bone in context.selected_pose_bones]
|
|
elif context.selected_objects:
|
|
new_matrices = [obj.matrix_world.copy() for obj in context.selected_objects]
|
|
|
|
#checking if the selection was moving and have different matrix
|
|
if last_matrices != new_matrices:
|
|
update_cursor_matrix(context)
|
|
last_matrices = new_matrices
|
|
|
|
#check if the cursor was moved
|
|
if 'cursor_matrix' not in globals():
|
|
cursor_matrix = context.scene.cursor.matrix
|
|
if context.scene.cursor.matrix == cursor_matrix:
|
|
return
|
|
assign_relative_cursor_prop(context)
|
|
|
|
def cursor_change_pre(self, context):
|
|
'''Depsgraph Handler to update cursor matrix if it was changed'''
|
|
context = bpy.context
|
|
global cursor_matrix, bone_selection, obj_selection, last_matrices
|
|
|
|
#check if selection was changed
|
|
if context.selected_pose_bones:
|
|
selected_items = context.selected_pose_bones
|
|
item_selection = bone_selection
|
|
else:
|
|
selected_items = context.selected_objects
|
|
item_selection = obj_selection
|
|
|
|
if set(selected_items) != set(item_selection):
|
|
if len(selected_items) > 1 or len(selected_items) < len(item_selection):
|
|
assign_relative_cursor_prop(context)
|
|
return
|
|
|
|
item = selected_items[0]
|
|
if 'relative_cursor' in item:
|
|
update_cursor_matrix(context)
|
|
else:
|
|
assign_relative_cursor_prop(context)
|
|
return
|
|
|
|
def get_matrix_avg(selected_items):
|
|
'''get the average between the matrices of all the selected items'''
|
|
if hasattr(selected_items[0], 'matrix'):
|
|
locs, rots, scales = zip(*(bone.matrix.decompose() for bone in selected_items))
|
|
else:
|
|
locs, rots, scales = zip(*(obj.matrix_world.decompose() for obj in selected_items))
|
|
|
|
translations = np.array(locs)
|
|
rotations = np.array(rots)
|
|
|
|
loc_avg = translations.mean(axis = 0)
|
|
rot_avg = rotations.mean(axis = 0)
|
|
rot_avg = mathutils.Quaternion(rot_avg)
|
|
|
|
return Matrix.LocRotScale(loc_avg, rot_avg, scales[0])
|
|
|
|
def update_cursor_matrix(context):
|
|
'''updating the new position of the Cursor'''
|
|
global cursor_matrix
|
|
selected_bones = context.selected_pose_bones
|
|
cursor = context.scene.cursor
|
|
if selected_bones:
|
|
item = selected_bones[0]
|
|
selected_items = selected_bones
|
|
else:
|
|
item = context.object
|
|
selected_items = context.selected_objects
|
|
|
|
# for bone in selected_bones:
|
|
if len(selected_items) > 1:
|
|
#multiple selection cursor is relative to the center of all the selections
|
|
item_matrix = get_matrix_avg(selected_items)
|
|
else:
|
|
if hasattr(item, 'matrix'):
|
|
item_matrix = item.id_data.matrix_world @ item.matrix
|
|
else:
|
|
item_matrix = item.matrix_world
|
|
|
|
#apply the new cursor position based on previous cursor
|
|
relative_matrix = Matrix(item['relative_cursor'])
|
|
cursor.matrix = item_matrix @ relative_matrix.inverted()
|
|
cursor_matrix = cursor.matrix
|
|
|
|
def assign_relative_matrix(context, items):
|
|
'''get the cursor relative matrices either from object or bones'''
|
|
|
|
cursor_matrix = context.scene.cursor.matrix
|
|
matrix_avg = get_matrix_avg(items)
|
|
|
|
for item in items:
|
|
item_matrix = item.id_data.matrix_world @ item.matrix if hasattr(item, 'matrix') else item.matrix_world
|
|
#check if it's a bone or object
|
|
if hasattr(item, 'matrix'):
|
|
item_matrix = item.id_data.matrix_world @ item.matrix
|
|
last_matrices.append(item.matrix.copy())
|
|
else:
|
|
item_matrix = item.matrix_world
|
|
last_matrices.append(item.matrix_world.copy())
|
|
|
|
item_matrix = matrix_avg if len(items) > 1 else item_matrix
|
|
matrix_dist = cursor_matrix.inverted() @ item_matrix
|
|
item['relative_cursor'] = matrix_dist
|
|
|
|
return last_matrices
|
|
|
|
def assign_relative_cursor_prop(context):
|
|
'''Updating the matrix property of the cursor distance on the bone'''
|
|
global last_matrices, bone_selection, obj_selection
|
|
|
|
last_matrices = []
|
|
|
|
if context.selected_pose_bones:
|
|
selected_items = context.selected_pose_bones
|
|
item_selection = bone_selection
|
|
else:
|
|
selected_items = context.selected_objects
|
|
item_selection = obj_selection
|
|
|
|
# cursor_matrix = context.scene.cursor.matrix
|
|
#Assigning the Matrix property
|
|
if selected_items:
|
|
assign_relative_matrix(context, selected_items)
|
|
|
|
if len(selected_items) >= len(item_selection):
|
|
return
|
|
|
|
#In case of Multiple controls, if controls are deselected, then apply the relative matrix also the previous selected controls
|
|
prev_items = set(item_selection) - set(selected_items)
|
|
assign_relative_matrix(context, list(prev_items))
|
|
|
|
class RelativeCursor(bpy.types.Operator):
|
|
"""Cursor moves relative to the selection to use as a Temp Pivot"""
|
|
bl_idname = "anim.relative_cursor"
|
|
bl_label = "Relative Cursor"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
# atb = context.scene.animtoolbox
|
|
ui = context.window_manager.atb_ui
|
|
# relative_cursor = ui.relative_cursor
|
|
# obj = context.object
|
|
# selected_bones = context.selected_pose_bones
|
|
|
|
global transform_pivot_point, cursor_matrix
|
|
|
|
#Turn off, remove the handlers
|
|
if ui.relative_cursor:
|
|
if relative_cursor_frame in bpy.app.handlers.frame_change_post:
|
|
bpy.app.handlers.frame_change_post.remove(relative_cursor_frame)
|
|
if cursor_change_pre in bpy.app.handlers.depsgraph_update_pre:
|
|
bpy.app.handlers.depsgraph_update_pre.remove(cursor_change_pre)
|
|
if cursor_change_post in bpy.app.handlers.depsgraph_update_post:
|
|
bpy.app.handlers.depsgraph_update_post.remove(cursor_change_post)
|
|
#quit the operator
|
|
if 'transform_pivot_point' in globals():
|
|
context.scene.tool_settings.transform_pivot_point = transform_pivot_point
|
|
del transform_pivot_point
|
|
del cursor_matrix
|
|
ui.relative_cursor = False
|
|
return {'FINISHED'}
|
|
|
|
#Turning on
|
|
ui.relative_cursor = True
|
|
transform_pivot_point = context.scene.tool_settings.transform_pivot_point
|
|
cursor_matrix = context.scene.cursor.matrix
|
|
context.scene.tool_settings.transform_pivot_point = 'CURSOR'
|
|
|
|
items = context.selected_pose_bones if context.selected_pose_bones else context.selected_objects
|
|
|
|
if items:
|
|
#if it's a single selection and already has a cursor assigned then don't re-assign
|
|
if len(items) != 1 or 'relative_cursor' not in items[0]:
|
|
assign_relative_cursor_prop(context)
|
|
|
|
#appending all the handlers
|
|
if relative_cursor_frame not in bpy.app.handlers.frame_change_post:
|
|
bpy.app.handlers.frame_change_post.append(relative_cursor_frame)
|
|
|
|
if cursor_change_pre not in bpy.app.handlers.depsgraph_update_pre:
|
|
bpy.app.handlers.depsgraph_update_pre.append(cursor_change_pre)
|
|
|
|
if cursor_change_post not in bpy.app.handlers.depsgraph_update_post:
|
|
bpy.app.handlers.depsgraph_update_post.append(cursor_change_post)
|
|
|
|
return {'FINISHED'}
|
|
|
|
class Keyframe_Offset(bpy.types.Operator):
|
|
"""Tool in the toolbar for interactive offsets keyframes"""
|
|
bl_idname = "anim.keyframe_offset"
|
|
bl_label = "Keyframe Offset"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
# scene = context.scene
|
|
# scene.animtoolbox.keyframes_offset +=0.1
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
self.mouse_x = event.mouse_x
|
|
# self.mouse_y = event.mouse_y
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
# print('inside modal', event.type, event.value)
|
|
scene = context.scene
|
|
value = 0
|
|
if event.mouse_x > self.mouse_x:
|
|
value = event.mouse_x - self.mouse_x
|
|
self.mouse_x = event.mouse_x
|
|
|
|
elif event.mouse_x < self.mouse_x:
|
|
value = event.mouse_x - self.mouse_x
|
|
self.mouse_x = event.mouse_x
|
|
|
|
scene.animtoolbox.keyframes_offset += value*0.01
|
|
|
|
if event.type in {'ESC'} or event.value == 'RELEASE': # Cancel
|
|
self.execute(context)
|
|
return {'FINISHED'}
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def keyframes_offset_slider(self, context):
|
|
'''interactive slider offsets keyframes and adds a property'''
|
|
if context.selected_objects is None:
|
|
return
|
|
global bone_selection, obj_selection
|
|
frame_offset = init_frame_offset = self.keyframes_offset
|
|
# for obj in context.selected_objects:
|
|
for obj in obj_selection:
|
|
if obj is None:
|
|
continue
|
|
# if obj.mode == 'POSE':
|
|
# continue
|
|
if obj.animation_data is None:
|
|
continue
|
|
if obj.animation_data.action is None:
|
|
continue
|
|
# if obj.mode =='OBJECT':
|
|
add_offset(obj, frame_offset, obj.animtoolbox.keyframes_offset)
|
|
obj.animtoolbox.keyframes_offset = frame_offset
|
|
frame_offset += init_frame_offset
|
|
|
|
if obj.mode != 'POSE':
|
|
continue
|
|
|
|
frame_offset_bone = init_frame_offset
|
|
for bone in bone_selection: #context.selected_pose_bones
|
|
if bone.name not in obj.pose.bones:
|
|
continue
|
|
#add the offset property to the bone
|
|
if 'keyframes_offset' not in bone:
|
|
bone['keyframes_offset'] = 0.0
|
|
bone_id = bone.path_from_id()
|
|
add_offset(obj, frame_offset_bone, bone['keyframes_offset'], bone_id)
|
|
bone['keyframes_offset'] = frame_offset_bone
|
|
|
|
frame_offset_bone += init_frame_offset
|
|
|
|
#if init_frame_offset is 0 then remove the keyframes offset properties
|
|
if not init_frame_offset:
|
|
remove_offset_property(context)
|
|
|
|
def remove_offset_property(context):
|
|
'removes the keyframes offset properties from the bones and objects'
|
|
for obj in context.selected_objects:
|
|
if obj.mode == 'POSE':
|
|
for bone in context.selected_pose_bones:
|
|
if 'keyframes_offset' in bone:
|
|
del bone['keyframes_offset']
|
|
obj.animtoolbox.keyframes_offset = 0
|
|
|
|
|
|
def add_offset(obj, offset, prev_offset, bone_id = None):
|
|
|
|
fcurves = get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
|
|
for fcu in fcurves:
|
|
if bone_id is not None:
|
|
if bone_id not in fcu.data_path:
|
|
continue
|
|
else:
|
|
#if it's object animation, then skip bone fcurves
|
|
if len(fcu.data_path.split(".")) != 1:
|
|
continue
|
|
|
|
if filter_properties(bpy.context.scene.animtoolbox, fcu):
|
|
continue
|
|
#create a sequence with all the keyframes
|
|
keyframes = np.zeros([len(fcu.keyframe_points)*2])
|
|
updated_left_handle = np.zeros([len(fcu.keyframe_points)*2])
|
|
updated_right_handle = np.zeros([len(fcu.keyframe_points)*2])
|
|
|
|
fcu.keyframe_points.foreach_get('co', keyframes)
|
|
fcu.keyframe_points.foreach_get('handle_left', updated_left_handle)
|
|
fcu.keyframe_points.foreach_get('handle_right', updated_right_handle)
|
|
|
|
if bpy.context.scene.animtoolbox.filter_keyframes:
|
|
select_control_points = np.zeros([len(fcu.keyframe_points)])
|
|
fcu.keyframe_points.foreach_get('select_control_point', select_control_points)
|
|
else:
|
|
select_control_points = 1
|
|
|
|
#add the new frame values only to the frames, every second item
|
|
keyframes[::2] += (offset - prev_offset) * select_control_points
|
|
updated_left_handle[::2] += (offset - prev_offset) * select_control_points
|
|
updated_right_handle[::2] += (offset - prev_offset) * select_control_points
|
|
|
|
#add the new values to all the keyframes altogether
|
|
fcu.keyframe_points.foreach_set('co', keyframes)
|
|
fcu.keyframe_points.foreach_set('handle_left', updated_left_handle)
|
|
fcu.keyframe_points.foreach_set('handle_right', updated_right_handle)
|
|
fcu.update()
|
|
|
|
def add_keyframe(bone):
|
|
|
|
obj = bone.id_data
|
|
|
|
if bone.rotation_mode == 'QUATERNION':
|
|
rotation = 'rotation_quaternion'
|
|
elif bone.rotation_mode == 'AXIS_ANGLE':
|
|
rotation = 'rotation_axis_angle'
|
|
else:
|
|
rotation = 'rotation_euler'
|
|
|
|
# rotation = 'rotation_quaternion' if bone.rotation_mode == 'QUATERNION' else 'rotation_euler'
|
|
transforms = ['location', rotation, 'scale']
|
|
path = bone.path_from_id() if obj.mode == 'POSE' else ''
|
|
groups = dict()
|
|
#find fcurves that are locked to exclude them
|
|
excluded_fcus = {}
|
|
if obj.animation_data:
|
|
if obj.animation_data.action:
|
|
fcurves = get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
for fcu in fcurves:
|
|
if path not in fcu.data_path or not fcu.lock:
|
|
continue
|
|
groups.update({(fcu.data_path, fcu.array_index ): fcu.group.name})
|
|
if fcu.data_path in excluded_fcus:
|
|
excluded_fcus[fcu.data_path].append(fcu.array_index)
|
|
else:
|
|
excluded_fcus.update({fcu.data_path : [fcu.array_index]})
|
|
|
|
#insert keyframe only to channels that are not excluded
|
|
for transform in transforms:
|
|
#path = bone.path_from_id() + '.' + transform
|
|
path = bone.path_from_id() + '.' + transform if obj.mode == 'POSE' else transform
|
|
# if path not in excluded_fcus:
|
|
# bone.keyframe_insert(transform)
|
|
# continue
|
|
length = len(getattr(bone, transform))
|
|
for i in range(length):
|
|
if path in excluded_fcus:
|
|
if i in excluded_fcus[path]:
|
|
continue
|
|
if (path, i) in groups:
|
|
group_name = groups[(path, i)]
|
|
else:
|
|
group_name = bone.name
|
|
|
|
bone.keyframe_insert(transform, index = i, group = group_name, frame = bpy.context.scene.frame_current_final)
|
|
|
|
def get_fcu_inbetweens(bone, frame, fcu_inbetweens):
|
|
|
|
obj = bone.id_data
|
|
|
|
if bone.rotation_mode == 'QUATERNION':
|
|
rotation = 'rotation_quaternion'
|
|
elif bone.rotation_mode == 'AXIS_ANGLE':
|
|
rotation = 'rotation_axis_angle'
|
|
else:
|
|
rotation = 'rotation_euler'
|
|
|
|
# rotation = 'rotation_quaternion' if bone.rotation_mode == 'QUATERNION' else 'rotation_euler'
|
|
transforms = ['location', rotation, 'scale']
|
|
# fcurves = obj.animation_data.action.fcurves
|
|
fcurves = get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
|
|
#insert keyframe only to channels that are not excluded
|
|
location, rotation, scale = bone.matrix_basis.decompose()
|
|
for transform in transforms:
|
|
path = bone.path_from_id() + '.' + transform if obj.mode == 'POSE' else transform
|
|
length = len(getattr(bone, transform))
|
|
|
|
for i in range(length):
|
|
fcu = fcurves.find(data_path = path, index = i)
|
|
if not fcu:
|
|
continue
|
|
if fcu.lock:
|
|
continue
|
|
if 'rotation' in transform:
|
|
if transform == 'rotation_euler':
|
|
coords = bone.matrix_basis.to_euler(bone.rotation_mode)
|
|
else:
|
|
coords = rotation
|
|
else:
|
|
coords = locals()[transform]
|
|
|
|
if fcu in fcu_inbetweens:
|
|
fcu_inbetweens[fcu].update({frame : coords[i]})
|
|
else:
|
|
fcu_inbetweens.update({fcu : {frame : coords[i]}})
|
|
|
|
return fcu_inbetweens
|
|
|
|
def is_writable(drv, attr):
|
|
try:
|
|
setattr(drv, attr, getattr(drv, attr))
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
def add_inbetween_key(self, context):
|
|
'''Adding a Breakdown keyframe that works also in Layers'''
|
|
obj = context.object
|
|
#anim_data = anim_data_type(obj)
|
|
anim_data = obj.animation_data
|
|
if anim_data is None:
|
|
return
|
|
if anim_data.action is None:
|
|
return
|
|
#strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
|
|
#frame = round(bake_ops.frame_evaluation(context.scene.frame_current, strip), 3)
|
|
frame = context.scene.frame_current
|
|
paths = [bone.path_from_id() for bone in context.selected_pose_bones]
|
|
fcurves = get_fcurves_channelbag(obj, anim_data.action)
|
|
for fcu in fcurves:
|
|
#filter selected bones
|
|
if obj.mode == 'POSE': #apply only to selected bones
|
|
#if obj.als.only_selected_bones:
|
|
if fcu.data_path.split('].')[0]+']' not in paths:
|
|
continue
|
|
if filter_properties(context.scene.animtoolbox, fcu):
|
|
continue
|
|
#get the last previous key
|
|
for keyframe in fcu.keyframe_points:
|
|
if round(keyframe.co[0], 3) > frame:
|
|
key_after = keyframe
|
|
break
|
|
elif round(keyframe.co[0], 3) < frame:
|
|
key_before = keyframe
|
|
else:
|
|
key_added = keyframe
|
|
|
|
if 'key_after' not in locals() or 'key_before' not in locals():
|
|
continue
|
|
if 'key_added' not in locals():
|
|
fcu.keyframe_points.add(1)
|
|
key_added = fcu.keyframe_points[-1]
|
|
value = key_before.co[1] + (key_after.co[1] - key_before.co[1]) * (self.inbetweener + 1)*0.5
|
|
key_added.co = (frame, value)
|
|
fcu.update()
|
|
del key_after
|
|
del key_before
|
|
del key_added
|
|
|
|
self['inbetweener'] = 0.0
|
|
|
|
def find_bone_in_datapath(datapath, context):
|
|
#iterating over the bones path from id
|
|
for bone in context.selected_pose_bones:
|
|
if bone.path_from_id() in datapath:
|
|
return bone
|
|
|
|
def get_matrix(obj, bone = None):
|
|
#get either matrix from bone or matrix world from object
|
|
if obj.mode == 'POSE':
|
|
matrix = bone.matrix.copy()
|
|
else:
|
|
matrix = obj.matrix_world.copy()
|
|
|
|
return matrix
|
|
|
|
def set_inbetween_matrix(self, source, matrix, matrix_other):
|
|
|
|
if self.inbetween_worldmatrix > 0:
|
|
matrix = matrix.copy().lerp(matrix_other, self.inbetween_worldmatrix)
|
|
elif self.inbetween_worldmatrix < 0:
|
|
matrix = matrix.copy().lerp(matrix_other, self.inbetween_worldmatrix*-1)
|
|
|
|
matrix = reverse_childof_constraint(source, matrix)
|
|
|
|
return matrix
|
|
|
|
def add_inbetween_worldmatrix(self, context):
|
|
'''Adding a Breakdown keyframe that works also in Layers'''
|
|
obj = context.object
|
|
if context.window_manager.atb_ui.is_dragging:
|
|
return
|
|
if obj is None:
|
|
self['inbetween_worldmatrix'] = 0.0
|
|
return
|
|
action = obj.animation_data.action
|
|
if action is None:
|
|
self['inbetween_worldmatrix'] = 0.0
|
|
return
|
|
# frame = round(context.scene.frame_current, 2)
|
|
|
|
if not self.inbetween_worldmatrix:
|
|
# scene.frame_set(frame)
|
|
return
|
|
|
|
if obj.mode == 'POSE' and not context.selected_pose_bones:
|
|
self['inbetween_worldmatrix'] = 0.0
|
|
return
|
|
bpy.ops.anim.inbetween_world_matrix('INVOKE_DEFAULT')
|
|
|
|
|
|
def get_matrix_other(self, context):
|
|
'''Get the matrix from the other frame, either previous or the next'''
|
|
|
|
scene = context.scene
|
|
for item, frames in self.items_frames.items():
|
|
#get the next and previous frames
|
|
next_frames = [frame for frame in frames if frame > self.frame]
|
|
if next_frames:
|
|
next_frame = next_frames[0]
|
|
|
|
prev_frames = [frame for frame in frames if frame < self.frame]
|
|
if prev_frames:
|
|
prev_frame = prev_frames[-1]
|
|
if not prev_frame and not next_frame:
|
|
continue
|
|
|
|
#get all the list of the frames from the different channels for each bone
|
|
#rest of the code is only for posebones
|
|
#creating a new dict to avoid frame_set at the same frame multiple times. It will get the matrix of multiple bones from each frame
|
|
#This is done just to avoid extra scene evaluation using frame_set
|
|
#adding the bone to the new flipped dict key using the frame number
|
|
prev_frame_bones = dict()
|
|
if prev_frame in prev_frame_bones:
|
|
prev_frame_bones[prev_frame].append(item)
|
|
else:
|
|
prev_frame_bones.update({prev_frame : [item]})
|
|
|
|
#the bones that are on the next frame
|
|
next_frame_bones = dict()
|
|
if next_frame in next_frame_bones:
|
|
next_frame_bones[next_frame].append(item)
|
|
else:
|
|
next_frame_bones.update({next_frame : [item]})
|
|
|
|
#if item.id_data.mode == 'POSE':
|
|
|
|
#Iterating the flipped dict with the frame as the key
|
|
for prev_frame, bones in prev_frame_bones.items():
|
|
# go to the frame
|
|
scene.frame_set(int(prev_frame), subframe = prev_frame % 1)
|
|
for bone in bones:
|
|
matrix_prev = get_matrix(bone.id_data, bone)
|
|
self.items_matrix_prev.update({bone : matrix_prev})
|
|
for next_frame, bones in next_frame_bones.items():
|
|
# go to the frame
|
|
scene.frame_set(int(next_frame), subframe = next_frame % 1)
|
|
for bone in bones:
|
|
matrix_next = get_matrix(bone.id_data, bone)
|
|
self.items_matrix_next.update({bone : matrix_next})
|
|
|
|
#going back to the original current frame
|
|
scene.frame_set(self.frame)
|
|
|
|
class InbetweenWorldMatrix(bpy.types.Operator):
|
|
"""Modal operator used while inbetween world matrix is running before release"""
|
|
bl_idname = "anim.inbetween_world_matrix"
|
|
bl_label = "Inbetween World Matrix"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def invoke(self, context, event):
|
|
#reset the values for dragging
|
|
self.stop = False
|
|
context.window_manager.atb_ui['is_dragging'] = True
|
|
if context.selected_pose_bones:
|
|
paths = [bone.path_from_id() for bone in context.selected_pose_bones]
|
|
|
|
self.atb = context.window_manager.atb_ui
|
|
self.frame = round(context.scene.frame_current, 2)
|
|
self.inbetween_worldmatrix = self.atb.inbetween_worldmatrix
|
|
|
|
#naming them as items because it is being used both for bones and objs
|
|
self.items_matrix_prev = dict()
|
|
self.items_matrix_next = dict()
|
|
|
|
self.items_matrix_org = dict()
|
|
# self.bones_other_frame = dict()
|
|
self.items_frames = dict()
|
|
|
|
for obj in context.selected_objects:
|
|
action = obj.animation_data.action
|
|
|
|
bone = None
|
|
|
|
fcurves = get_fcurves_channelbag(obj, action)
|
|
for fcu in fcurves:
|
|
#filter selected bones
|
|
if obj.mode == 'POSE': #apply only to selected bones
|
|
if fcu.data_path.split('].')[0]+']' not in paths:
|
|
continue
|
|
else:
|
|
#get the related bone
|
|
bone = find_bone_in_datapath(fcu.data_path, context)
|
|
bonename = fcu.data_path.split('"')[1]
|
|
if bonename in obj.pose.bones:
|
|
bone = obj.pose.bones[bonename]
|
|
|
|
if bone not in self.items_matrix_org:
|
|
self.items_matrix_org.update({bone : bone.matrix.copy()})
|
|
|
|
else:
|
|
if obj not in self.items_matrix_org:
|
|
self.items_matrix_org.update({obj : obj.matrix_world.copy()})
|
|
bone = obj
|
|
|
|
#get all the frames from the fcurve
|
|
length = len(fcu.keyframe_points) * 2
|
|
keyframes = np.zeros(length)
|
|
fcu.keyframe_points.foreach_get('co', keyframes)
|
|
frames = sorted([round(frame, 2) for frame in keyframes[::2]])
|
|
#record all the frames
|
|
if bone in self.items_frames:
|
|
self.items_frames[bone] += frames
|
|
self.items_frames[bone] = sorted(set(self.items_frames[bone]))
|
|
else:
|
|
self.items_frames.update({bone : frames})
|
|
|
|
get_matrix_other(self, context)
|
|
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
scene = context.scene
|
|
if self.stop:
|
|
context.window_manager.atb_ui['is_dragging'] = False
|
|
selected_items = context.selected_pose_bones if context.selected_pose_bones else context.selected_objects
|
|
for item in selected_items:
|
|
if scene.tool_settings.use_keyframe_insert_auto:
|
|
add_keyframe(item)
|
|
self.atb['inbetween_worldmatrix'] = 0
|
|
redraw_areas(['PROPERTIES'])
|
|
#modal is being cancelled because of undo issue with the modal running through the property
|
|
return {'FINISHED'}
|
|
|
|
if event.value == 'RELEASE': # Stop the modal on next frame. Don't block the event since we want to exit the field dragging
|
|
self.stop = True
|
|
|
|
if self.inbetween_worldmatrix != self.atb.inbetween_worldmatrix:
|
|
self.inbetween_worldmatrix = self.atb.inbetween_worldmatrix
|
|
|
|
#Add the inbetween value that was calculated during the invoke
|
|
if self.inbetween_worldmatrix > 0:
|
|
items_matrix_other = self.items_matrix_next
|
|
elif self.inbetween_worldmatrix < 0:
|
|
items_matrix_other = self.items_matrix_prev
|
|
else:
|
|
return {'PASS_THROUGH'}
|
|
|
|
for bone, matrix_other in items_matrix_other.items():
|
|
matrix = set_inbetween_matrix(self, bone, self.items_matrix_org[bone], matrix_other)
|
|
matrix = filter_matrix_properties(context, self.items_matrix_org[bone], matrix)
|
|
if bone.id_data.mode == 'POSE':
|
|
bone.matrix = matrix
|
|
else:
|
|
bone.matrix_world = matrix
|
|
|
|
if scene.tool_settings.use_keyframe_insert_auto:
|
|
add_keyframe(bone)
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
def find_mirror_bone(bone):
|
|
digit = ''
|
|
name = bone.name
|
|
#find if there is a digit number in the end of the name and separate it
|
|
if name.split('.')[-1].isdigit():
|
|
digit = '.' + name.split('.')[-1]
|
|
name = ''.join(name.split('.')[:-1])
|
|
|
|
#All the conventions and separators
|
|
mirror_conventions = {'L' : 'R', 'R' : 'L', 'l' : 'r', 'r' : 'l', 'Right':'Left', 'Left':'Right', 'right' : 'left', 'left' : 'right', 'LEFT' : 'RIGHT', 'RIGHT' : 'LEFT'}
|
|
indexes = (0, -1)
|
|
separators = ('.', '_', '-', ' ')
|
|
rig = bone.id_data
|
|
|
|
for separator in separators:
|
|
for i in indexes:
|
|
if name.split(separator)[i] in mirror_conventions:
|
|
side = name.split(separator)[i]
|
|
mirror_side = mirror_conventions[side]
|
|
mirror_name = name.replace(side, mirror_side)
|
|
if mirror_name + digit in rig.pose.bones:
|
|
return rig.pose.bones[mirror_name + digit]
|
|
|
|
startend = {'Right':'Left', 'Left':'Right', 'right' : 'left', 'left' : 'right', 'LEFT' : 'RIGHT', 'RIGHT' : 'LEFT'}
|
|
for side, mirror_side in startend.items():
|
|
if name.endswith(side) or name.startswith(side):
|
|
mirror_name = name.replace(side, mirror_side)
|
|
if mirror_name + digit in rig.pose.bones:
|
|
return rig.pose.bones[mirror_name + digit]
|
|
|
|
return None
|
|
|
|
def blend_to_mirror(self, context):
|
|
'''blend to mirrored pose'''
|
|
|
|
if context.window_manager.atb_ui.is_dragging:
|
|
return
|
|
else:
|
|
bpy.ops.anim.blend_to_mirror('INVOKE_DEFAULT')
|
|
|
|
class BlendToMirroModal(bpy.types.Operator):
|
|
"""Modal operator used while blend to mirror is running before release"""
|
|
bl_idname = "anim.blend_to_mirror"
|
|
bl_label = "Blend_To_Mirror"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
#stop: bpy.props.BoolProperty() # This is used so we don't end up in an infinite loop because we blocked the release event
|
|
|
|
def invoke(self, context, event):
|
|
self.stop = False
|
|
self.ui = context.window_manager.atb_ui
|
|
context.window_manager.atb_ui['is_dragging'] = True
|
|
#assign the initial matrix to each bone
|
|
for bone in context.selected_pose_bones:
|
|
bone['matrix'] = bone.matrix_basis
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
scene = context.scene
|
|
#finishing after release
|
|
if self.stop:
|
|
context.window_manager.atb_ui['is_dragging'] = False
|
|
for bone in context.selected_pose_bones:
|
|
if scene.tool_settings.use_keyframe_insert_auto:
|
|
add_keyframe(bone)
|
|
if 'matrix' in bone:
|
|
del bone['matrix']
|
|
self.ui['blend_mirror'] = 0
|
|
redraw_areas(['PROPERTIES'])
|
|
#modal is being cancelled because of undo issue with the modal running through the property
|
|
return {'FINISHED'}
|
|
|
|
if event.value == 'RELEASE': # Stop the modal on next frame. Don't block the event since we want to exit the field dragging
|
|
self.stop = True
|
|
|
|
for bone in context.selected_pose_bones:
|
|
mirror_bone = find_mirror_bone(bone)
|
|
if mirror_bone is None:
|
|
continue
|
|
mirror_bone_matrix = mirror_bone.matrix_basis.copy()
|
|
mirror_plane = Matrix.Scale(-1, 4, (1, 0, 0))
|
|
mirrored_matrix = mirror_plane @ mirror_bone_matrix @ mirror_plane
|
|
matrix = Matrix(bone['matrix']).lerp(mirrored_matrix, self.ui.blend_mirror)
|
|
matrix = filter_matrix_properties(context, bone.matrix_basis, matrix)
|
|
bone.matrix_basis = matrix
|
|
|
|
#self['blend_mirror'] = 0
|
|
return {'PASS_THROUGH'}
|
|
|
|
class ApplyKeyframesOffset(bpy.types.Operator):
|
|
"""Offset keyframes for every selected object"""
|
|
bl_idname = "anim.apply_keyframes_offset"
|
|
bl_label = "Apply the keyframe offset"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return len(context.selected_objects)
|
|
|
|
def execute(self, context):
|
|
remove_offset_property(context)
|
|
context.scene.animtoolbox.keyframes_offset = 0
|
|
|
|
return {'FINISHED'}
|
|
|
|
class SelectKeyframesOffset(bpy.types.Operator):
|
|
|
|
"""Offset keyframes for every selected object"""
|
|
bl_idname = "anim.select_keyframes_offset"
|
|
bl_label = "Select_keyframes_offset"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
# if not hasattr(bpy.types.Object, 'keyframes_offset') and not hasattr(bpy.types.PoseBone, 'keyframes_offset'):
|
|
# return {'CANCELLED'}
|
|
for obj in bpy.data.objects:
|
|
if obj.animtoolbox.keyframes_offset:
|
|
obj.select_set(True)
|
|
if obj.type == 'ARMATURE':
|
|
posemode = False
|
|
for bone in obj.pose.bones:
|
|
if 'keyframes_offset' in bone:
|
|
#check if there are bones with an offset
|
|
posemode = True
|
|
obj.data.bones[bone.name].select = True
|
|
if posemode and obj.mode != 'POSE':
|
|
context.view_layer.objects.active = obj
|
|
bpy.ops.object.mode_set(mode = 'POSE')
|
|
|
|
return {'FINISHED'}
|
|
|
|
def get_frame_range(context, obj):
|
|
#get the frame range
|
|
frame_range = []
|
|
inbetweens = []
|
|
atb = context.scene.animtoolbox
|
|
if atb.range_type == 'CURRENT':
|
|
frame_range = [context.scene.frame_current]
|
|
|
|
elif atb.range_type == 'RANGE':
|
|
#make sure that the first frame is before the second
|
|
if atb.bake_frame_start > atb.bake_frame_end:
|
|
atb.bake_frame_start = atb.bake_frame_end
|
|
frame_range = list(range(atb.bake_frame_start, atb.bake_frame_end+1))
|
|
|
|
elif atb.range_type == 'SELECTED':
|
|
if obj.mode == 'POSE':
|
|
bones_paths = [bone.path_from_id() for bone in context.selected_pose_bones]
|
|
if obj.animation_data is None:
|
|
return frame_range
|
|
if obj.animation_data.action is None:
|
|
return frame_range
|
|
|
|
smartframes = set()
|
|
|
|
fcurves = get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
for fcu in fcurves:
|
|
path = fcu.data_path.split('"].')[0] + '"]'
|
|
if obj.mode == 'POSE' and path not in bones_paths:
|
|
continue
|
|
for keyframe in fcu.keyframe_points:
|
|
smartframes.add(round(keyframe.co[0], 2))
|
|
if keyframe.select_control_point:
|
|
frame_range.append(round(keyframe.co[0], 2))
|
|
|
|
smartframes = sorted(smartframes)
|
|
frame_range = sorted(set(frame_range))
|
|
|
|
#getting the inbetweens
|
|
smartframes += add_inbetweens(list(smartframes))
|
|
smartframes = sorted(smartframes)
|
|
|
|
#getting the inbetweens within the frame range
|
|
inbetweens = []
|
|
for frame in frame_range:
|
|
index = smartframes.index(frame)
|
|
if frame == smartframes[-1]:
|
|
continue
|
|
if smartframes[index+1] in frame_range or smartframes[index+2] in frame_range:
|
|
continue
|
|
inbetweens.append(smartframes[index+1])
|
|
inbetweens.append(smartframes[index+2])
|
|
#merge inbetweens into the frame range
|
|
# frame_range = sorted(frame_range + inbetweens)
|
|
|
|
return frame_range, inbetweens
|
|
|
|
def notification_invoke(self, context, text = 'Copy Matrix', size = 20.0):
|
|
'''get ready and lauch the notification'''
|
|
wm = context.window_manager
|
|
self.timer = wm.event_timer_add(0.1, window=context.window)
|
|
self.no_timer = False
|
|
self.fade_duration = 1.0
|
|
self.max_duration = 3
|
|
self.text = text
|
|
self.size = size
|
|
self.fade_out_start = 0
|
|
self.draw_handle = bpy.types.SpaceView3D.draw_handler_add(draw_text_callback_px, (self, context), 'WINDOW', 'POST_PIXEL')
|
|
redraw_areas(['VIEW_3D'])
|
|
|
|
def update_notification(self, context, event, esc = False, fade_out = True):
|
|
'''notify with a modal operator that the matrix was being copied'''
|
|
|
|
if not hasattr(self, 'draw_handle'):
|
|
return {'FINISHED'}
|
|
|
|
if event.type in {'ESC'} or esc:
|
|
bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW')
|
|
context.area.tag_redraw()
|
|
context.window_manager.event_timer_remove(self.timer)
|
|
del self.draw_handle
|
|
return {'FINISHED'}
|
|
|
|
if event.type == 'TIMER':
|
|
# print('timer running in the bg', self.scale, self.rotate)
|
|
#check if the fade in finished and remove the timer
|
|
if self.timer.time_duration >= self.fade_duration:
|
|
self.no_timer = True
|
|
|
|
redraw_areas(['VIEW_3D'])
|
|
if fade_out:
|
|
if self.timer.time_duration > self.max_duration and fade_out:
|
|
bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW')
|
|
|
|
context.window_manager.event_timer_remove(self.timer)
|
|
return {'FINISHED'}
|
|
# else:
|
|
# redraw_areas(['VIEW_3D'])
|
|
|
|
if (self.max_duration - self.timer.time_duration) <= self.fade_duration and not self.fade_out_start and fade_out:
|
|
self.fade_out_start = self.timer.time_duration + 0.001
|
|
|
|
# if self.timer.time_duration >= self.fade_duration and not self.fade_out_start:
|
|
# self.fade_out_start = self.timer.time_duration + 0.001
|
|
|
|
return {'PASS_THROUGH'}
|
|
|
|
class CopyMatrix(bpy.types.Operator):
|
|
"""Copy the matrix of the selection and store it"""
|
|
bl_idname = "anim.copy_matrix"
|
|
bl_label = "Copy"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object is not None
|
|
|
|
def invoke(self, context, event):
|
|
|
|
notification_invoke(self, context)
|
|
context.window_manager.modal_handler_add(self)
|
|
self.execute(context)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
|
|
return_modal = update_notification(self, context, event)
|
|
|
|
return return_modal
|
|
|
|
def execute(self, context):
|
|
obj = context.object
|
|
global matrix, objs_matrix
|
|
if 'matrix' in globals():
|
|
del matrix
|
|
if 'objs_matrix' in globals():
|
|
del objs_matrix
|
|
|
|
objs_matrix = dict()
|
|
|
|
for obj in context.selected_objects:
|
|
if obj.mode == 'POSE':
|
|
if not context.selected_pose_bones:
|
|
return {'CANCELLED'}
|
|
for bone in context.selected_pose_bones:
|
|
if bone.id_data != obj:
|
|
continue
|
|
|
|
bone_matrix = obj.matrix_world @ bone.matrix.copy()
|
|
|
|
if obj.name in objs_matrix:
|
|
objs_matrix[obj.name].update({bone.name : bone_matrix})
|
|
else:
|
|
objs_matrix.update({obj.name : {bone.name : bone_matrix}})
|
|
|
|
if bone == context.active_pose_bone:
|
|
matrix = bone_matrix
|
|
|
|
else:
|
|
matrix = obj.matrix_world.copy()
|
|
objs_matrix.update({obj.name : matrix})
|
|
|
|
return {'FINISHED'}
|
|
|
|
class PasteMatrix(bpy.types.Operator):
|
|
"""paste the matrix of the selection"""
|
|
bl_idname = "anim.paste_matrix"
|
|
bl_label = "Paste"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object is not None
|
|
|
|
def execute(self, context):
|
|
#obj = context.object
|
|
global matrix, objs_matrix
|
|
if 'matrix' not in globals():
|
|
return {'CANCELLED'}
|
|
scene = context.scene
|
|
frame_current_final = scene.frame_current_final
|
|
|
|
bones_matrices = dict()
|
|
constrained = set()
|
|
#constraint evaluation is
|
|
# con.target.matrix_world.copy() @ con.inverse_matrix @ basis
|
|
#to flip it I use
|
|
# con.inverse_matrix.inverted() @ con.target.matrix_world.copy().inverted() @ matrix
|
|
# to find the basis (Matrix.Identity(4) + con.target.matrix_world @ con.inverse_matrix).inverted() @ (2 * matrix)
|
|
|
|
#finding the influence
|
|
# using for location and rotation obj.matrix_basis = (Matrix.Identity(4) + 0.3 * (con.target.matrix_world @ con.inverse_matrix - Matrix.Identity(4))).inverted() @ matrix
|
|
# using for scale obj.matrix_basis = (Matrix.Identity(4).lerp(offset, 0.7)).inverted() @ matrix
|
|
#formula for the rest of the constraints
|
|
# diff = basis.inverted() @ obj.matrix_parent_inverse.inverted() @ obj.parent.matrix_world.inverted() @ matrix_world
|
|
# obj.matrix_basis = obj.matrix_parent_inverse.inverted() @ obj.parent.matrix_world.inverted() @ matrix @ diff.inverted()
|
|
|
|
#apply the new matrix with the difference
|
|
for obj in context.selected_objects:
|
|
frame_range, inbetweens = get_frame_range(context, obj)
|
|
fcu_inbetweens = dict()
|
|
for frame in sorted(frame_range + inbetweens):
|
|
scene.frame_set(int(frame), subframe = frame % 1)
|
|
if obj.mode == 'POSE':
|
|
#first get all the bones and matrices before writing back
|
|
for bone in context.selected_pose_bones:
|
|
matrix_copied = matrix
|
|
if bone.id_data.name in objs_matrix:
|
|
if bone.name in objs_matrix[bone.id_data.name]:
|
|
matrix_copied = objs_matrix[bone.id_data.name][bone.name]
|
|
|
|
matrix_copied = obj.matrix_world.inverted() @ matrix_copied
|
|
#Store the matrices for each bone that will use it
|
|
matrix_copied = reverse_childof_constraint(bone, matrix_copied, constrained)
|
|
|
|
#use matrix for bones without constraints
|
|
bones_matrices.update({bone : matrix_copied})
|
|
|
|
#Reordering the bones, so that we apply first the matrix offset to the constrained bones
|
|
bones_matrices = reorder_bones_matrices(bones_matrices, constrained)
|
|
|
|
paste_bones_matrices(bones_matrices, constrained)
|
|
|
|
#adding the keyframes
|
|
for bone in bones_matrices.keys():
|
|
paste_keyframes_get_inbetweens(scene, bone, inbetweens, frame, frame_range, fcu_inbetweens)
|
|
|
|
else:
|
|
target = obj
|
|
#drivers, constraints = rec_drivers_constraints(target)
|
|
matrix_copied = reverse_childof_constraint(target, matrix, constrained)
|
|
if target not in constrained:
|
|
matrix_copied = filter_matrix_properties(context, target.matrix_world, matrix_copied)
|
|
target.matrix_world = matrix_copied
|
|
|
|
if target in constrained:
|
|
context.view_layer.update()
|
|
matrix_copied = reverse_constraint_offset(target.matrix_world, matrix_copied)
|
|
target.matrix_world = filter_matrix_properties(context, target.matrix_world, matrix_copied)
|
|
|
|
paste_keyframes_get_inbetweens(scene, target, inbetweens, frame, frame_range, fcu_inbetweens)
|
|
|
|
if inbetweens and len(frame_range) > 1:
|
|
add_interpolations(fcu_inbetweens.keys(), fcu_inbetweens, frames = frame_range[:-1])
|
|
|
|
if context.scene.frame_current_final != frame_current_final:
|
|
scene.frame_current = int(frame_current_final)
|
|
scene.frame_subframe = frame_current_final % 1
|
|
|
|
#Turn off markers frame range if it' activated
|
|
if context.scene.animtoolbox.bake_frame_range:
|
|
context.scene.animtoolbox.bake_frame_range = False
|
|
return {'FINISHED'}
|
|
|
|
class CopyRelativeMatrix(bpy.types.Operator):
|
|
"""Copy the relative distance matrix from active between two objects or bones"""
|
|
bl_idname = "anim.copy_relative_matrix"
|
|
bl_label = "Copy_Relative_Matrix"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object is not None
|
|
|
|
def invoke(self, context, event):
|
|
obj = context.object
|
|
if context.selected_pose_bones:
|
|
if len(context.selected_pose_bones) < 2 and len(context.selected_objects) < 2:
|
|
return {'CANCELLED'}
|
|
else:
|
|
if len(context.selected_objects) < 2:
|
|
return {'CANCELLED'}
|
|
|
|
selection = context.active_pose_bone.name if context.active_pose_bone and obj.mode == 'POSE' else context.active_object.name
|
|
notification_invoke(self, context, text = 'Copy Matrix relative to ' + selection)
|
|
context.window_manager.modal_handler_add(self)
|
|
self.execute(context)
|
|
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def modal(self, context, event):
|
|
|
|
return_modal = update_notification(self, context, event)
|
|
return return_modal
|
|
|
|
def execute(self, context):
|
|
# obj = context.object
|
|
global objs_matrix_dist, matrix_dist, source_active_name, source_rig_name
|
|
if 'matrix_dist' in globals():
|
|
del matrix_dist
|
|
if 'source_active_name' in globals():
|
|
del source_active_name
|
|
if 'source_rig_name' in globals():
|
|
del source_rig_name
|
|
if 'objs_matrix_dist' in globals():
|
|
del objs_matrix_dist
|
|
|
|
#create a dictionary for all the realtive distance of the bones and objects
|
|
objs_matrix_dist = dict()
|
|
|
|
#get the source bone or object
|
|
if context.active_pose_bone:
|
|
source_active = context.active_pose_bone
|
|
source_rig_name = source_active.id_data.name
|
|
source_matrix = source_active.matrix
|
|
else:
|
|
source_active = context.active_object
|
|
source_matrix = source_active.matrix_world
|
|
|
|
source_active_name = source_active.name
|
|
|
|
for obj in context.selected_objects:
|
|
if obj.type == 'ARMATURE':
|
|
#Get the distance from the selected bones
|
|
for bone_relative in obj.pose.bones:
|
|
if not bone_relative.bone.select:
|
|
continue
|
|
if bone_relative == source_active:
|
|
continue
|
|
|
|
#Adding the offset from the armature transform both for the active and relative
|
|
rig_offset = obj.matrix_world
|
|
if source_active.id_data.type == 'ARMATURE':
|
|
rig_offset = source_active.id_data.matrix_world.inverted() @ rig_offset
|
|
|
|
matrix_dist = source_matrix.inverted() @ rig_offset @ bone_relative.matrix
|
|
|
|
#store each bone matrix distance in a dictionary
|
|
if obj.name in objs_matrix_dist:
|
|
objs_matrix_dist[obj.name].update({bone_relative.name : matrix_dist})
|
|
else:
|
|
objs_matrix_dist.update({obj.name : {bone_relative.name : matrix_dist}})
|
|
|
|
else:
|
|
if obj == source_active:
|
|
continue
|
|
matrix_dist = source_matrix.inverted() @ obj.matrix_world
|
|
objs_matrix_dist.update({obj.name : matrix_dist})
|
|
|
|
return {'FINISHED'}
|
|
|
|
def paste_keyframes_get_inbetweens(scene, target, inbetweens, frame, frame_range, fcu_inbetweens):
|
|
#if autokey is turned on then add a keyframe
|
|
if (scene.tool_settings.use_keyframe_insert_auto or scene.animtoolbox.range_type != 'CURRENT') and 'target' in locals():
|
|
if frame not in inbetweens:
|
|
add_keyframe(target)
|
|
#store the inbetween values
|
|
elif len(frame_range) > 1:
|
|
fcu_inbetweens = get_fcu_inbetweens(target, frame, fcu_inbetweens)
|
|
|
|
def reverse_bone_constraints(context, bone, matrix_copied):
|
|
|
|
matrix_copied = reverse_constraint_offset(bone.matrix, matrix_copied)
|
|
|
|
matrix_copied = filter_matrix_properties(context, bone.matrix, matrix_copied)
|
|
|
|
matrix = bone.id_data.matrix_world.inverted() @ matrix_copied
|
|
|
|
return matrix
|
|
|
|
def reverse_constraint_offset(item_matrix, matrix_copied):
|
|
'''adding the offset of all the bones
|
|
that have constraints that are not child of'''
|
|
|
|
con_offset = item_matrix.inverted() @ matrix_copied
|
|
matrix_copied = item_matrix @ con_offset @ con_offset
|
|
|
|
return matrix_copied
|
|
|
|
def reverse_childof_constraint(source, matrix_source, constrained = set()):
|
|
|
|
obj = source.id_data
|
|
if not len(source.constraints):
|
|
return matrix_source #@ obj.matrix_world.inverted()
|
|
|
|
offsets = []
|
|
offsets_lerp = []
|
|
offset_inv = Matrix.Identity(4)
|
|
offset_inv_lerp = Matrix.Identity(4)
|
|
|
|
#If the source is a bone then get the Armature matrix to add to the calculation
|
|
if type(source) == bpy.types.PoseBone:
|
|
obj_offset = obj.matrix_world
|
|
else:
|
|
obj_offset = Matrix.Identity(4)
|
|
|
|
#iterate and store all the inverted offsets of all the child constraints
|
|
for con in source.constraints:
|
|
if con.mute or not con.influence:
|
|
continue
|
|
if hasattr(con, 'target') and con.target is None:
|
|
continue
|
|
if con.type != 'CHILD_OF':
|
|
constrained.add(source)
|
|
continue
|
|
if con.subtarget == '':
|
|
parent_matrix = obj_offset.inverted() @ con.target.matrix_world
|
|
#remove obj.matrix_world when connected to an object
|
|
offset = parent_matrix @ con.inverse_matrix - Matrix.Identity(4)
|
|
#offset for the scale with influence already included
|
|
offset_lerp = Matrix.Identity(4).lerp(parent_matrix @ con.inverse_matrix, con.influence)
|
|
else:
|
|
|
|
parent_matrix = con.target.pose.bones[con.subtarget].matrix
|
|
|
|
if con.target != obj:
|
|
parent_matrix = obj_offset.inverted() @ con.target.matrix_world @ parent_matrix
|
|
|
|
#Include armature object matrix
|
|
offset = parent_matrix @ con.inverse_matrix - Matrix.Identity(4)
|
|
offset_lerp = Matrix.Identity(4).lerp(parent_matrix @ con.inverse_matrix, con.influence)
|
|
|
|
#Adding the influence to the offset
|
|
offset = Matrix.Identity(4) + con.influence * offset
|
|
|
|
offset_inv = offset_inv @ offset.inverted()
|
|
offset_inv_lerp = offset_inv_lerp @ offset_lerp.inverted()
|
|
|
|
offsets.append(offset)
|
|
|
|
if not offsets:
|
|
return matrix_source
|
|
|
|
#final Matrix values
|
|
matrix_basis = offset_inv @ matrix_source
|
|
matrix_lerp = offset_inv_lerp @ matrix_source
|
|
loc, rot, scale = matrix_basis.decompose()
|
|
loc_lerp, rot_lerp, scale_lerp = matrix_lerp.decompose()
|
|
|
|
matrix_basis = Matrix.LocRotScale(loc, rot_lerp, scale_lerp)
|
|
|
|
return matrix_basis
|
|
|
|
def reorder_bones_matrices(bones_matrices, constrained):
|
|
#Reordering the bones, so that we apply first the matrix offset to the constrained bones
|
|
re_bones_matrices = {bone: matrix for bone, matrix in bones_matrices.items() if bone in constrained}
|
|
re_bones_matrices.update({bone: matrix for bone, matrix in bones_matrices.items() if bone not in constrained})
|
|
|
|
return re_bones_matrices
|
|
|
|
def paste_bone_matrix(bone, matrix_copied, constrained, x_filter = True):
|
|
#running again separatly in case the bones are in a hierarchy and influencing each other
|
|
# for bone, matrix_copied in bones_matrices.items():
|
|
# Determine whether to use bone.matrix or bone.matrix_world
|
|
if hasattr(bone, 'matrix'): # Check if it's a bone
|
|
matrix_attr = 'matrix'
|
|
elif hasattr(bone, 'matrix_world'): # Check if it's an object
|
|
matrix_attr = 'matrix_world'
|
|
else:
|
|
raise AttributeError("The provided 'bone' does not have a valid matrix attribute.")
|
|
|
|
context = bpy.context
|
|
if x_filter : matrix_copied = filter_matrix_properties(context, getattr(bone, matrix_attr), matrix_copied)
|
|
# bone.matrix = bone.id_data.matrix_world.inverted() @ matrix_copied
|
|
setattr(bone, matrix_attr, matrix_copied) # bone.id_data.matrix_world.inverted() @
|
|
if bone not in constrained:
|
|
# filter_matrix_properties(context, bone.matrix, matrix_copied)
|
|
return
|
|
context.view_layer.update()
|
|
|
|
matrix_copied = reverse_bone_constraints(context, bone, matrix_copied)
|
|
if x_filter : matrix_copied = filter_matrix_properties(context, bone.matrix, matrix_copied)
|
|
# bone.matrix = matrix_copied
|
|
setattr(bone, matrix_attr, matrix_copied)
|
|
#important to have the extra update
|
|
context.view_layer.update()
|
|
|
|
def paste_bones_matrices(bones_matrices, constrained, x_filter = True):
|
|
#running again separatly in case the bones are in a hierarchy and influencing each other
|
|
for bone, matrix_copied in bones_matrices.items():
|
|
paste_bone_matrix(bone, matrix_copied, constrained, x_filter)
|
|
|
|
class PasteRelativeMatrix(bpy.types.Operator):
|
|
"""paste the relative matrix of the selection"""
|
|
bl_idname = "anim.paste_relative_matrix"
|
|
bl_label = "Paste_Relative"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object is not None
|
|
|
|
def execute(self, context):
|
|
obj = context.object
|
|
scene = context.scene
|
|
frame_current = scene.frame_current
|
|
global objs_matrix_dist, matrix_dist, source_active_name, source_rig_name
|
|
if 'objs_matrix_dist' not in globals():
|
|
return {'CANCELLED'}
|
|
|
|
source_rig = None
|
|
source_bone = None
|
|
source_obj = None
|
|
bones_matrices = dict()
|
|
constrained = set()
|
|
#get the source matrix
|
|
if 'source_rig_name' in globals():
|
|
if source_rig_name in bpy.data.objects:
|
|
source_rig = bpy.data.objects[source_rig_name]
|
|
source_obj = source_rig
|
|
source_bone = source_rig.pose.bones[source_active_name]
|
|
#Get the current matrix of the source bone
|
|
matrix_source = source_bone.matrix
|
|
|
|
elif 'source_active_name' in globals():
|
|
if source_active_name in bpy.data.objects:
|
|
source_obj = bpy.data.objects[source_active_name]
|
|
matrix_source = source_obj.matrix_world
|
|
else:
|
|
return {'CANCELLED'}
|
|
|
|
if source_obj is None and source_rig is None:
|
|
return {'CANCELLED'}
|
|
|
|
for obj in context.selected_objects:
|
|
#if the source object was in object mode during copy and now it's pose mode then quit
|
|
frame_range, inbetweens = get_frame_range(context, obj)
|
|
fcu_inbetweens = dict()
|
|
|
|
|
|
for frame in sorted(frame_range+inbetweens):
|
|
scene.frame_set(int(frame))
|
|
if obj.mode == 'POSE':
|
|
for bone in context.selected_pose_bones:
|
|
if bone.id_data != obj:
|
|
continue
|
|
#check that the selected bone is not the source bone
|
|
if bone == source_bone:
|
|
continue
|
|
|
|
bone_matrix_dist = matrix_dist
|
|
#get the matrix from the bone itself
|
|
if bone.id_data.name in objs_matrix_dist:
|
|
if bone.name in objs_matrix_dist[bone.id_data.name]:
|
|
bone_matrix_dist = objs_matrix_dist[bone.id_data.name][bone.name]
|
|
|
|
#Adding the offset from the armature transform both for the active and relative
|
|
#If it's the same Armature it will cancel each other
|
|
rig_offset = obj.matrix_world.inverted()
|
|
if source_rig:
|
|
rig_offset = rig_offset @ source_rig.matrix_world
|
|
matrix_new = rig_offset @ matrix_source @ bone_matrix_dist
|
|
|
|
#Store the matrices for each bone that will use it
|
|
matrix_new = reverse_childof_constraint(bone, matrix_new, constrained)
|
|
|
|
bones_matrices.update({bone : matrix_new})
|
|
|
|
#Reordering the bones, so that we apply first the matrix offset to the constrained bones
|
|
bones_matrices = reorder_bones_matrices(bones_matrices, constrained)
|
|
|
|
paste_bones_matrices(bones_matrices, constrained)
|
|
|
|
for bone in bones_matrices.keys():
|
|
paste_keyframes_get_inbetweens(scene, bone, inbetweens, frame, frame_range, fcu_inbetweens)
|
|
|
|
else:
|
|
target = obj
|
|
#check that the selected obj is not the source object
|
|
if source_active_name in bpy.data.objects and 'source_rig_name' not in globals():
|
|
if target == bpy.data.objects[source_active_name]:
|
|
continue
|
|
|
|
if obj.name in objs_matrix_dist:
|
|
obj_matrix_dist = objs_matrix_dist[obj.name]
|
|
else:
|
|
obj_matrix_dist = matrix_dist
|
|
|
|
matrix_new = matrix_source @ obj_matrix_dist
|
|
matrix_new = reverse_childof_constraint(target, matrix_new)
|
|
if target not in constrained:
|
|
matrix_new = filter_matrix_properties(context, target.matrix_world, matrix_new)
|
|
target.matrix_world = matrix_new
|
|
|
|
if target in constrained:
|
|
context.view_layer.update()
|
|
matrix_new = reverse_constraint_offset(target.matrix_world, matrix_new)
|
|
target.matrix_world = filter_matrix_properties(context, target.matrix_world, matrix_new)
|
|
|
|
paste_keyframes_get_inbetweens(scene, target, inbetweens, frame, frame_range, fcu_inbetweens)
|
|
|
|
if inbetweens and len(frame_range) > 1:
|
|
add_interpolations(fcu_inbetweens.keys(), fcu_inbetweens, frames = frame_range[:-1])
|
|
|
|
#return to the original frame in case of using frame range
|
|
if context.scene.frame_current != frame_current:
|
|
scene.frame_current = frame_current
|
|
|
|
#Turn off markers frame range if it' activated
|
|
if context.scene.animtoolbox.bake_frame_range:
|
|
context.scene.animtoolbox.bake_frame_range = False
|
|
return {'FINISHED'}
|
|
|
|
def copy_paste_world_update(self, context):
|
|
if self.copy_paste_world:
|
|
self.copy_paste_relative = not self.copy_paste_world
|
|
|
|
def copy_paste_relative_update(self, context):
|
|
if self.copy_paste_relative:
|
|
self.copy_paste_world = not self.copy_paste_relative
|
|
|
|
def sharekeys_add_missing_fcurves(obj, attr_index, fcurves):
|
|
transformations = ["rotation_quaternion","rotation_euler", "rotation_axis_angle", "location", "scale"]
|
|
|
|
#creating a set of tuple pairs with the data path and indexes
|
|
datapaths_arrays = {(fcu.data_path, fcu.array_index) for fcu in fcurves}
|
|
|
|
for attr, index in attr_index:
|
|
if not hasattr(obj, attr):
|
|
continue
|
|
if (attr, index) in datapaths_arrays:
|
|
continue
|
|
if attr not in transformations and bpy.context.scene.animtoolbox.filter_custom_props:
|
|
continue
|
|
|
|
#check filter attributes, and continue if it's filtered
|
|
f_transform = 'filter_rotation' if 'rotation' in attr else 'filter_' + attr
|
|
if hasattr(bpy.context.scene.animtoolbox, f_transform):
|
|
f_attr = getattr(bpy.context.scene.animtoolbox, f_transform)
|
|
if index < len(f_attr):
|
|
if f_attr[index]:
|
|
continue
|
|
|
|
if type(obj) == bpy.types.PoseBone:
|
|
#obj in this case is actually a bone
|
|
group = obj.name
|
|
path = obj.path_from_id() + '.' + attr if attr in transformations else obj.path_from_id() + attr
|
|
else:
|
|
group = 'Object Transforms' if attr in transformations else ''
|
|
path = attr
|
|
|
|
action = obj.id_data.animation_data.action
|
|
|
|
#Get the container, either action of channelbag because of adding groups
|
|
fcurves_container = get_fcurves_container(obj, action)
|
|
|
|
if 'rotation' in attr:
|
|
#converting the rotation depending on the rotation mode
|
|
mode = 'euler' if len(obj.rotation_mode) == 3 else obj.rotation_mode.lower()
|
|
path_index = path.find('rotation_')
|
|
rot = 'rotation_' + mode
|
|
path = path[:path_index] + rot
|
|
|
|
if rot == 'rotation_euler' and index == 3:
|
|
#skipping the last index for euler rotation
|
|
continue
|
|
elif rot != 'rotation_euler' and (path, 3) not in datapaths_arrays:
|
|
#adding an extra curve for quaternion or axis_angle
|
|
extra_fcu = fcurves_container.fcurves.new(data_path = path, index = 3, action_group = group)
|
|
fcurves.append(extra_fcu)
|
|
datapaths_arrays.add((extra_fcu.data_path, extra_fcu.array_index))
|
|
|
|
if (path, index) in datapaths_arrays:
|
|
continue
|
|
|
|
fcu = fcurves_container.fcurves.new(data_path = path, index = index)
|
|
add_group_to_fcurve(obj.id_data, fcu, group)
|
|
fcurves.append(fcu)
|
|
datapaths_arrays.add((fcu.data_path, fcu.array_index))
|
|
|
|
return fcurves
|
|
|
|
def share_keyframes(fcu, frames):
|
|
|
|
for keyframe in fcu.keyframe_points:
|
|
if bpy.context.scene.animtoolbox.filter_keyframes and not keyframe.select_control_point:
|
|
continue
|
|
if keyframe.co[0] not in frames.keys():
|
|
frames.update({keyframe.co[0] : (keyframe.interpolation, keyframe.handle_right_type, keyframe.handle_left_type)})
|
|
|
|
return frames
|
|
|
|
def get_fcurves_frames(selection, fcurves, all_fcurves, frames, attr_index):
|
|
|
|
#get all the paths from the bones
|
|
bone_paths = {bone.path_from_id() for bone in selection if type(bone) == bpy.types.PoseBone}
|
|
# found_paths = set()
|
|
|
|
# transformations = ["rotation_quaternion","rotation_euler", "rotation_axis_angle", "location", "scale"]
|
|
|
|
for fcu in fcurves:
|
|
# print('fcu ', fcu)
|
|
if not bone_paths and not fcu.data_path.startswith('pose.bones'):
|
|
#in case it's just an object or data
|
|
all_fcurves.append(fcu)
|
|
frames = share_keyframes(fcu, frames)
|
|
|
|
#update the dictionary with all the datapath and indeces
|
|
attr_index.add((fcu.data_path, fcu.array_index))
|
|
continue
|
|
|
|
#check bones
|
|
if not fcu.data_path.startswith('pose.bones'):
|
|
continue
|
|
if not '"]' in fcu.data_path:
|
|
continue
|
|
path = fcu.data_path.split('"]')[0] + '"]'
|
|
if path not in bone_paths:
|
|
continue
|
|
|
|
#get the attribute from the data path
|
|
attr = fcu.data_path.replace(path,'')
|
|
if attr[0] == '.' : attr = attr[1:]
|
|
attr_index.add((attr, fcu.array_index))
|
|
|
|
all_fcurves.append(fcu)
|
|
frames = share_keyframes(fcu, frames)
|
|
|
|
|
|
return frames, attr_index
|
|
|
|
class ShareKeys(bpy.types.Operator):
|
|
"""Share keyframes between all the selected objects and bones"""
|
|
bl_idname = "anim.share_keyframes"
|
|
bl_label = "Share_Keyframes"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object is not None
|
|
|
|
def add_slot(self, obj, action):
|
|
'''can be replaced with add_slot_to_animdata(anim_data)'''
|
|
|
|
slot = add_action_slot(obj, action)
|
|
if hasattr(obj.animation_data, 'action_slot'):
|
|
obj.animation_data.action_slot = slot
|
|
|
|
def get_fcurves_set(self, obj_actions):
|
|
fcurves = set()
|
|
for obj, action in obj_actions.items():
|
|
fcurves = fcurves.union(set(get_fcurves_channelbag(obj, action)))
|
|
return fcurves
|
|
|
|
def add_get_action(self, obj):
|
|
'''Adding action if it doesnt exist, can be used both for obj and obj.data'''
|
|
if obj.animation_data is None:
|
|
obj.animation_data_create()
|
|
if obj.animation_data.action is None:
|
|
action_data = bpy.data.actions.new(obj.data.name)
|
|
obj.animation_data.action = action_data
|
|
action = obj.animation_data.action
|
|
self.add_slot(obj, action)
|
|
else:
|
|
action = obj.animation_data.action
|
|
|
|
return action
|
|
|
|
def execute(self, context):
|
|
#Get the frames and fcurves
|
|
all_fcurves =[]
|
|
frames = dict()
|
|
attr_index_bone = set()
|
|
attr_index_obj = set()
|
|
actions_data_types = set()
|
|
selected_bones = context.selected_pose_bones
|
|
if selected_bones:
|
|
objs = {bone.id_data for bone in selected_bones}
|
|
obj_actions = {obj : obj.animation_data.action for obj in objs if obj.animation_data is not None}
|
|
#get the fcurves from the bones
|
|
fcurves = self.get_fcurves_set(obj_actions)
|
|
|
|
frames, attr_index_bone = get_fcurves_frames(selected_bones, fcurves, all_fcurves, frames, attr_index_bone)
|
|
|
|
#Apply to objects that are not in pose mode or not armatures
|
|
for obj in context.selected_objects:
|
|
if obj.mode == 'POSE':
|
|
continue
|
|
#if there is no animation data or an action then create it
|
|
action = self.add_get_action(obj)
|
|
fcurves = get_fcurves_channelbag(obj, action)
|
|
frames, attr_index_obj = get_fcurves_frames([obj], fcurves, all_fcurves, frames, attr_index_obj)
|
|
|
|
if obj.data.animation_data is None:
|
|
continue
|
|
if not obj.data.animation_data.action:
|
|
continue
|
|
#add the type of object if the data is also animated
|
|
actions_data_types.add(obj.type)
|
|
fcurves = get_fcurves_channelbag(obj.data, obj.data.animation_data.action)
|
|
frames, attr_index_obj = get_fcurves_frames([obj], fcurves, all_fcurves, frames, attr_index_obj)
|
|
|
|
#Getting all the available transforms and array index
|
|
attr_index_bone = sorted(attr_index_bone, key=lambda x: (x[0], x[1]))
|
|
attr_index_obj = sorted(attr_index_obj, key=lambda x: (x[0], x[1]))
|
|
|
|
####add missing fcurves to objects####
|
|
obj_fcurves = dict()
|
|
if selected_bones and attr_index_bone:
|
|
for bone in selected_bones:
|
|
action_bone = self.add_get_action(bone.id_data)
|
|
bone_path = bone.path_from_id()
|
|
bone_fcurves = [fcu for fcu in all_fcurves if fcu.id_data == action_bone and bone_path in fcu.data_path]
|
|
obj_fcurves.update({bone: bone_fcurves})
|
|
obj_fcurves[bone] = sharekeys_add_missing_fcurves(bone, attr_index_bone, obj_fcurves[bone])
|
|
|
|
if attr_index_obj:
|
|
for obj in context.selected_objects:
|
|
action = obj.animation_data.action
|
|
obj_fcurves.update({obj: [fcu for fcu in all_fcurves if fcu.id_data == action]})
|
|
obj_fcurves[obj] = sharekeys_add_missing_fcurves(obj, attr_index_obj, obj_fcurves[obj])
|
|
|
|
#add animation to data if exists in other same type of objects
|
|
if obj.type in actions_data_types:
|
|
action_data = self.add_get_action(obj.data)
|
|
|
|
# print('udating objfcurves animation data')
|
|
obj_fcurves.update({obj.data: [fcu for fcu in all_fcurves if fcu.id_data == action_data]})
|
|
obj_fcurves[obj.data] = sharekeys_add_missing_fcurves(obj.data, attr_index_obj, obj_fcurves[obj.data])
|
|
|
|
####write all the keyframes####
|
|
for obj, fcurves in obj_fcurves.items():
|
|
for fcu in fcurves:
|
|
if filter_properties(context.scene.animtoolbox, fcu):
|
|
continue
|
|
found_frames = [keyframe.co[0] for keyframe in fcu.keyframe_points]
|
|
for frame, interpolation in frames.items():
|
|
if frame in found_frames:
|
|
continue
|
|
|
|
if len(fcu.keyframe_points):
|
|
# value = fcu.evaluate(frame)
|
|
keyframe = fcu.keyframe_points.insert(frame = frame, value = fcu.evaluate(frame))
|
|
else:
|
|
#Get the value from the bone or object
|
|
if type(obj) == bpy.types.PoseBone:
|
|
#Removing the bones path from the data path
|
|
attr = fcu.data_path.replace(obj.path_from_id(),'')
|
|
if attr[0] == '.' : attr = attr[1:]
|
|
value = getattr(obj, attr)
|
|
else:
|
|
value = getattr(obj, fcu.data_path)
|
|
if not isinstance(value, (int, float)):
|
|
value = value[fcu.array_index]
|
|
keyframe = fcu.keyframe_points.insert(frame = frame, value = value)
|
|
keyframe.interpolation = interpolation[0]
|
|
|
|
fcu.update()
|
|
|
|
redraw_areas({'DOPESHEET_EDITOR', 'GRAPH_EDITOR'})
|
|
return {'FINISHED'}
|
|
|
|
def rot_mode_to_channel(to_rot_mode):
|
|
'''convert rotation mode to fcurve path transform'''
|
|
if len(to_rot_mode) == 3:
|
|
to_rot_mode_fcu = 'rotation_euler'
|
|
elif to_rot_mode == 'QUATERNION':
|
|
to_rot_mode_fcu = 'rotation_quaternion'
|
|
else:
|
|
to_rot_mode_fcu = 'rotation_axis_angle'
|
|
|
|
return to_rot_mode_fcu
|
|
|
|
#################################################
|
|
# actual euler filter
|
|
|
|
def euler_to_string(e):
|
|
return "%.2f, %.2f, %.2f" % (r(e[0]), r(e[1]), r(e[2]))
|
|
|
|
|
|
def degrees(a):
|
|
return a / 360.0 * 2 * pi
|
|
|
|
|
|
def d(a):
|
|
return degrees(a)
|
|
|
|
|
|
def r(a):
|
|
return a / (2 * pi) * 360.0
|
|
|
|
|
|
def wrap_angle(a):
|
|
return (a + pi) % (2 * pi) - pi
|
|
|
|
|
|
def euler_distance(e1, e2):
|
|
return abs(e1[0] - e2[0]) + abs(e1[1] - e2[1]) + abs(e1[2] - e2[2])
|
|
|
|
|
|
def euler_axis_index(axis):
|
|
if axis == 'X':
|
|
return 0
|
|
if axis == 'Y':
|
|
return 1
|
|
if axis == 'Z':
|
|
return 2
|
|
return None
|
|
|
|
|
|
def flip_euler(euler, rotation_mode):
|
|
ret = euler.copy()
|
|
inner_axis = rotation_mode[0]
|
|
outer_axis = rotation_mode[2]
|
|
middle_axis = rotation_mode[1]
|
|
|
|
ret[euler_axis_index(inner_axis)] += pi
|
|
ret[euler_axis_index(outer_axis)] += pi
|
|
ret[euler_axis_index(middle_axis)] *= -1
|
|
ret[euler_axis_index(middle_axis)] += pi
|
|
return ret
|
|
|
|
|
|
def naive_flip_diff(a1, a2):
|
|
while abs(a1 - a2) > pi:
|
|
if a1 < a2:
|
|
a2 -= 2 * pi
|
|
else:
|
|
a2 += 2 * pi
|
|
|
|
return a2
|
|
|
|
|
|
def euler_filter(kfs, rotation_mode):
|
|
if len(kfs) <= 1:
|
|
return kfs
|
|
# prev = kfs[0]["rotation_euler"]
|
|
# ret = [{"key": kfs[0]["key"],
|
|
# "rotation_euler": prev.copy()}]
|
|
prev = kfs[0]
|
|
ret = [prev.copy()]
|
|
for i in range(1, len(kfs)):
|
|
e = kfs[i].copy()
|
|
e[0] = naive_flip_diff(prev[0], e[0])
|
|
e[1] = naive_flip_diff(prev[1], e[1])
|
|
e[2] = naive_flip_diff(prev[2], e[2])
|
|
|
|
fe = flip_euler(e, rotation_mode)
|
|
fe[0] = naive_flip_diff(prev[0], fe[0])
|
|
fe[1] = naive_flip_diff(prev[1], fe[1])
|
|
fe[2] = naive_flip_diff(prev[2], fe[2])
|
|
|
|
de = euler_distance(prev, e)
|
|
dfe = euler_distance(prev, fe)
|
|
# print("distance: %s, flipped distance: %s Euler %s frame %s" % (de, dfe, kfs[i], i))
|
|
|
|
if dfe < de:
|
|
e = fe
|
|
prev = e
|
|
ret.append(e)
|
|
# ret += [{"key": kfs[i]["key"],
|
|
# "rotation_euler": e}]
|
|
return ret
|
|
|
|
|
|
#################################################
|
|
|
|
def add_inbetweens(smartframes):
|
|
inbetweens = []
|
|
for i, frame in enumerate(smartframes[:-1]):
|
|
# if (smartframes[i+1] - frame) <= 1:
|
|
# continue
|
|
if (smartframes[i+1] - frame) <= 1:
|
|
continue
|
|
|
|
inbetweens.append(round(frame + (smartframes[i+1] - frame)*1/3, 2))
|
|
inbetweens.append(round(frame + (smartframes[i+1] - frame)*2/3, 2))
|
|
inbetweens.sort()
|
|
|
|
# all_frames = sorted(self.frames + self.inbetweens)
|
|
return inbetweens
|
|
|
|
def add_interpolations(fcurves, fcu_inbetweens, frames = None):
|
|
|
|
inbetweens = sorted(set([frame for inbetweens in fcu_inbetweens.values() for frame in inbetweens.keys()]))
|
|
|
|
#turn inbetween keyframes values to handles
|
|
for fcu in fcurves:
|
|
fcu.update()
|
|
#the index for the inbetweens
|
|
P1index = 0
|
|
P2index = 1
|
|
keys = fcu.keyframe_points
|
|
|
|
for i, key in enumerate(keys[:-1]):# or inbetweens[P1index] is None:
|
|
if frames:
|
|
if round(key.co[0], 2) not in frames:
|
|
continue
|
|
|
|
if round(keys[i+1].co[0], 2) - round(keys[i].co[0], 2) <= 1:
|
|
keys[i].handle_right_type = 'AUTO'
|
|
keys[i+1].handle_left_type = 'AUTO'
|
|
fcu.update()
|
|
continue
|
|
|
|
if key.interpolation != 'BEZIER':
|
|
P1index += 2
|
|
P2index += 2
|
|
continue
|
|
|
|
P0 = round(keys[i].co[1], 2)
|
|
P3 = round(keys[i+1].co[1], 2)
|
|
|
|
|
|
P1 = fcu_inbetweens[fcu][inbetweens[P1index]]
|
|
P2 = fcu_inbetweens[fcu][inbetweens[P2index]]
|
|
|
|
cp1 = (1/6)*( -5*P0 + 18*P1 - 9*P2 + 2*P3)
|
|
cp2 = (1/6)*( 2*P0 - 9*P1 +18*P2 - 5*P3)
|
|
|
|
#apply handle types
|
|
keys[i].handle_right_type = 'FREE'
|
|
keys[i].handle_left_type = 'FREE'
|
|
keys[i+1].handle_right_type = 'FREE'
|
|
keys[i+1].handle_left_type = 'FREE'
|
|
|
|
keys[i].handle_right = [inbetweens[P1index], cp1]
|
|
keys[i+1].handle_left = [inbetweens[P2index], cp2]
|
|
#iterate through the inbetween smartkeys
|
|
P1index += 2
|
|
P2index += 2
|
|
|
|
fcu.update()
|
|
|
|
class FindRotationMode(bpy.types.Operator):
|
|
"""Recommend an euler rotation to avoid gimbal lock"""
|
|
bl_idname = "anim.find_rotation_mode"
|
|
bl_label = "Find an Euler Rotation Mode"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
|
|
def invoke(self, context, event):
|
|
selected_bones = context.selected_pose_bones
|
|
if not selected_bones:
|
|
return {'CANCELLED'}
|
|
|
|
smartframes = set()
|
|
rotation_modes = ('XYZ', 'XZY', 'YXZ', 'YZX', 'ZXY', 'ZYX')
|
|
axis_index = {'X' : 0, 'Y' : 1, 'Z' : 2}
|
|
self.bone_rot_modes = dict()
|
|
|
|
for posebone in selected_bones:
|
|
if not posebone.id_data.animation_data:
|
|
continue
|
|
if not posebone.id_data.animation_data.action:
|
|
continue
|
|
#get the transform from the original rot mode
|
|
transform = rot_mode_to_channel(posebone.rotation_mode)
|
|
# data_path = posebone.path_from_id() + '.' + transform
|
|
keyframes = emp.get_bone_keyframes(posebone, transform)
|
|
smartframes = sorted(set(map(lambda x: round(x, 2), keyframes[::2])))
|
|
#store all the euler rotations of all the different options
|
|
mid_axis_values = dict()
|
|
axis_eulers = dict()
|
|
for to_rot_mode in rotation_modes:
|
|
rot_mode = posebone.rotation_mode
|
|
obj = posebone.id_data
|
|
# fcurves = obj.animation_data.action.fcurves
|
|
fcurves = get_fcurves_channelbag(obj, obj.animation_data.action)
|
|
new_rot_eulers = []
|
|
|
|
#get the values from the original rotation
|
|
for frame in smartframes:
|
|
new_rot = convert_rotation(posebone, fcurves, frame, to_rot_mode)
|
|
new_rot_eulers.append(new_rot)
|
|
|
|
#apply Euler filter
|
|
# new_rot_eulers = euler_filter(new_rot_eulers, to_rot_mode)
|
|
|
|
axis_eulers.update({to_rot_mode : new_rot_eulers})
|
|
#get a dictionary with only the mid axis values
|
|
mix_axis_index = axis_index[to_rot_mode[1]]
|
|
mid_axis_values.update({to_rot_mode : [abs(rot[mix_axis_index]) for rot in new_rot_eulers]})
|
|
|
|
# get the axis that has the least 90 degrees on the mid axis
|
|
self.rotation_90_counts = dict()
|
|
for rot_mode, values in mid_axis_values.items():
|
|
avg_diff = average_difference(values, 1.57)
|
|
self.rotation_90_counts.update({rot_mode : avg_diff})
|
|
#sort the dictionary using the values from of the avg_diff
|
|
self.rotation_90_counts = dict(sorted(self.rotation_90_counts.items(), key=lambda item: item[1], reverse=True))
|
|
self.bone_rot_modes.update({posebone.name : self.rotation_90_counts})
|
|
|
|
context.scene.animtoolbox.rotation_mode = list(self.rotation_90_counts.keys())[0]
|
|
|
|
if not self.bone_rot_modes:
|
|
return {'CANCELLED'}
|
|
wm = context.window_manager
|
|
return wm.invoke_props_dialog(self, width=300) #+ max([len(bone.name) for bone in selected_bones])
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
row = layout.row()
|
|
row.alignment = 'CENTER'
|
|
row.label(text = 'Avoid gimbal locks!')
|
|
row = layout.row()
|
|
row.alignment = 'CENTER'
|
|
row.label(text = 'Rotation modes listed from best to worst')
|
|
for bonename, rot_modes in self.bone_rot_modes.items():
|
|
row = layout.split(factor = 0.4)
|
|
# row.alignment = 'CENTER'
|
|
row.label(text = 'bone' + bonename + ':', icon = 'BONE_DATA')
|
|
# row = layout.row()
|
|
# row.alignment = 'CENTER'
|
|
rot_list = ''.join([rot + ', ' for rot in rot_modes])
|
|
row.label(text = rot_list)
|
|
|
|
|
|
def execute(self, context):
|
|
return {'CANCELLED'}
|
|
|
|
def average_difference(lst, target):
|
|
return np.mean(np.abs(np.array(lst) - target))
|
|
|
|
def convert_rotation(posebone, fcurves, frame, to_rot_mode):
|
|
|
|
#get the transform from the original rot mode
|
|
transform = rot_mode_to_channel(posebone.rotation_mode)
|
|
data_path = posebone.path_from_id() + '.' + transform
|
|
# to_rot_mode = context.scene.animtoolbox.rotation_mode
|
|
to_rot_mode_fcu = rot_mode_to_channel(to_rot_mode)
|
|
|
|
#define array length
|
|
from_rot_range = 3 if len(posebone.rotation_mode) == 3 else 4
|
|
|
|
#Get the original rotation
|
|
rot = []
|
|
for i in range(from_rot_range):
|
|
#get the original fcurve
|
|
fcu = fcurves.find(data_path, index = i)
|
|
if fcu is None:
|
|
continue
|
|
rot.append(fcu.evaluate(frame))
|
|
if len(posebone.rotation_mode) == 3:
|
|
org_rot = mathutils.Euler(rot, posebone.rotation_mode)
|
|
else:
|
|
org_rot = mathutils.Quaternion(rot)
|
|
|
|
#get the new value as Euler or quaternions
|
|
if transform == 'rotation_euler':
|
|
if to_rot_mode_fcu != 'rotation_euler':
|
|
new_rot = org_rot.to_quaternion()
|
|
else:
|
|
matrix = org_rot.to_matrix()
|
|
new_rot = matrix.to_euler(to_rot_mode)
|
|
|
|
elif transform == 'rotation_quaternion':
|
|
if to_rot_mode_fcu == 'rotation_euler':
|
|
new_rot = org_rot.to_euler(to_rot_mode)
|
|
elif to_rot_mode_fcu == 'rotation_axis_angle':
|
|
new_rot = org_rot.to_euler(to_rot_mode)
|
|
|
|
elif transform == 'rotation_axis_angle':
|
|
if to_rot_mode_fcu == 'rotation_euler':
|
|
new_rot = org_rot.to_euler(to_rot_mode)
|
|
elif to_rot_mode_fcu == 'rotation_quaternion':
|
|
new_rot = org_rot.to_quaternion()
|
|
|
|
return new_rot
|
|
|
|
class ConvertRotationMode(bpy.types.Operator):
|
|
"""Convert the rotation keyframes to a new rotation mode using smartbake"""
|
|
bl_idname = "anim.convert_rotation_mode"
|
|
bl_label = "Convert Rotation Mode"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
# @classmethod
|
|
# def poll(cls, context):
|
|
# return context.object.type == 'ARMATURE'
|
|
|
|
def execute(self, context):
|
|
scene = context.scene
|
|
selected_bones = context.selected_pose_bones
|
|
# smartframes = set()
|
|
|
|
to_rot_mode = scene.animtoolbox.rotation_mode
|
|
to_rot_mode_fcu = rot_mode_to_channel(to_rot_mode)
|
|
|
|
#get the keyframes from the bones
|
|
for posebone in selected_bones:
|
|
|
|
if to_rot_mode == posebone.rotation_mode:
|
|
continue
|
|
obj = posebone.id_data
|
|
# fcurves = obj.animation_data.action.fcurves
|
|
action = obj.animation_data.action
|
|
fcurves = get_fcurves_channelbag(obj, action)
|
|
fcu_inbetweens = dict()
|
|
fcu_keyframes = dict()
|
|
#get the transform from the original rot mode
|
|
transform = rot_mode_to_channel(posebone.rotation_mode)
|
|
# data_path = posebone.path_from_id() + '.' + transform
|
|
|
|
keyframes = emp.get_bone_keyframes(posebone, transform)
|
|
|
|
#get all interpolations and handle types of the keyframes
|
|
handle_types = emp.get_bone_keyframes(posebone, transform, property = 'interpolation')
|
|
interpolations = handle_types[::3]
|
|
handle_left_type = handle_types[1::3]
|
|
handle_right_type = handle_types[2::3]
|
|
|
|
smartframes = sorted(set(map(lambda x: round(x, 2), keyframes[::2])))
|
|
inbetweens = add_inbetweens(smartframes)
|
|
all_frames = sorted(smartframes + inbetweens)
|
|
|
|
new_path = posebone.path_from_id() + '.' + to_rot_mode_fcu
|
|
|
|
#define array length
|
|
to_rot_range = 3 if to_rot_mode_fcu == 'rotation_euler' else 4
|
|
# from_rot_range = 3 if len(posebone.rotation_mode) == 3 else 4
|
|
|
|
#create or find he fcurves
|
|
new_fcurves = []
|
|
group = None
|
|
|
|
for i in range(to_rot_range):
|
|
#find the target fcurve or create a new one
|
|
fcu_new = fcurves.find(data_path = new_path, index = i)
|
|
|
|
#start fresh
|
|
if fcu_new is None:
|
|
fcu_new = fcurves.new(data_path = new_path, index = i)
|
|
if group is None:
|
|
group = add_group_to_fcurve(obj, fcu_new, posebone.name)
|
|
else:
|
|
fcu_new.group = group
|
|
new_fcurves.append(fcu_new)
|
|
|
|
new_rot_eulers = []
|
|
|
|
#get all the rotations in a list
|
|
for frame in all_frames:
|
|
new_rot = convert_rotation(posebone, fcurves, frame, to_rot_mode)
|
|
new_rot_eulers.append(new_rot)
|
|
|
|
#apply Euler filter
|
|
if to_rot_mode_fcu == 'rotation_euler':
|
|
new_rot_eulers = euler_filter(new_rot_eulers, to_rot_mode)
|
|
|
|
#get the values from the original rotation
|
|
for frame, new_rot in zip(all_frames, new_rot_eulers):
|
|
|
|
for i in range(to_rot_range):
|
|
new_fcu = new_fcurves[i]
|
|
|
|
if frame in smartframes:
|
|
if new_fcu in fcu_keyframes:
|
|
frame_new_rot = fcu_keyframes[new_fcu]
|
|
frame_new_rot.update({frame : new_rot[i]})
|
|
fcu_keyframes[new_fcu].update(frame_new_rot)
|
|
else:
|
|
fcu_keyframes.update({new_fcu : {frame : new_rot[i]}})
|
|
|
|
# Storing the inbetween values in dictionaries
|
|
elif frame in inbetweens:
|
|
if new_fcu in fcu_inbetweens:
|
|
frame_new_rot = fcu_inbetweens[new_fcu]
|
|
frame_new_rot.update({frame : new_rot[i]})
|
|
fcu_inbetweens[new_fcu].update(frame_new_rot)
|
|
else:
|
|
fcu_inbetweens.update({new_fcu : {frame : new_rot[i]}})
|
|
|
|
#write the keyframes
|
|
for i in range(to_rot_range):
|
|
new_fcu = new_fcurves[i]
|
|
for frame_index, frame in enumerate(smartframes):
|
|
found_key = False
|
|
#find the keyframe
|
|
for keyframe in new_fcu.keyframe_points:
|
|
if keyframe.co[0] == frame:
|
|
found_key = True
|
|
break
|
|
|
|
#if not found add a new one
|
|
if not found_key:
|
|
new_fcu.keyframe_points.add(1)
|
|
keyframe = new_fcu.keyframe_points[-1]
|
|
|
|
keyframe.co = (frame, fcu_keyframes[new_fcu][frame])
|
|
keyframe.interpolation = interpolations[frame_index]
|
|
keyframe.handle_left_type = handle_left_type[frame_index]
|
|
keyframe.handle_right_type = handle_right_type[frame_index]
|
|
|
|
new_fcu.update()
|
|
|
|
add_interpolations(new_fcurves, fcu_inbetweens)
|
|
|
|
posebone.rotation_mode = to_rot_mode
|
|
|
|
return {'FINISHED'}
|
|
|
|
def bake_range_type(self, context):
|
|
#start bake range with 10 frames range around the current frame
|
|
if self.range_type != "RANGE":
|
|
return
|
|
if self.bake_frame_end:
|
|
return
|
|
self.bake_frame_start = context.scene.frame_current - 5
|
|
self.bake_frame_end = context.scene.frame_current + 5
|
|
|
|
def bake_frame_end_limit(self, context):
|
|
#property update - limit the frame start to be smaller then frame end
|
|
if self.bake_frame_start > self.bake_frame_end:
|
|
self.bake_frame_start = self.bake_frame_end
|
|
|
|
def bake_frame_start_limit(self, context):
|
|
#property update - limit the frame start to be smaller then frame end
|
|
if self.bake_frame_start > self.bake_frame_end:
|
|
self.bake_frame_end = self.bake_frame_start
|
|
|
|
def filter_matrix_properties(context, original_matrix, new_matrix):
|
|
'''Filter Matrix Values'''
|
|
scene = context.scene
|
|
atb = scene.animtoolbox
|
|
if not any(list(atb.filter_location) + list(atb.filter_rotation)[1:] + list(atb.filter_scale)):
|
|
return new_matrix
|
|
|
|
org_location, org_rotation, org_scale = original_matrix.decompose()
|
|
new_location, new_rotation, new_scale = new_matrix.decompose()
|
|
org_rotation = original_matrix.to_euler()
|
|
new_rotation = new_matrix.to_euler()
|
|
|
|
for transform in ('location', 'rotation', 'scale'):
|
|
filter_attr = getattr(atb, 'filter_' + transform)
|
|
#in case of rotation use only xyz, instead of the W
|
|
if transform == 'rotation':
|
|
filter_attr = filter_attr[1:]
|
|
for i in range(len(filter_attr)):
|
|
if not filter_attr[i]:
|
|
continue
|
|
#Get the original value into the new variable
|
|
locals()['new_' + transform][i] = locals()['org_' + transform][i]
|
|
|
|
return Matrix.LocRotScale(new_location, new_rotation, new_scale)
|
|
|
|
def filter_properties(context, fcu):
|
|
'Filter the W X Y Z attributes of the transform properties for fcurves'
|
|
|
|
transformations = ["rotation_quaternion","rotation_euler", "rotation_axis_angle", "location", "scale"]
|
|
#check if the fcurve data path ends with any of the transformations
|
|
if not any(fcu.data_path.endswith(transform) for transform in transformations):
|
|
return True if context.filter_custom_props else False
|
|
|
|
transform = fcu.data_path.split('"].')[1] if '"].' in fcu.data_path else fcu.data_path
|
|
|
|
index = fcu.array_index
|
|
|
|
if 'rotation' in transform :
|
|
transform = 'rotation'
|
|
|
|
f_transform = 'filter_' + transform
|
|
|
|
#in case of channels like bbone_scalein that are no included then return
|
|
if not hasattr(context, f_transform):
|
|
return True
|
|
|
|
attr = getattr(context, f_transform)
|
|
|
|
if 'rotation' in transform:
|
|
#when baking to ctrls it is using only 3 arrays in the constraints
|
|
if not fcu.data_path.endswith('rotation_euler') and len(attr) == 3:
|
|
index -= 1
|
|
elif fcu.data_path.endswith('rotation_euler') and len(attr) == 4:
|
|
index += 1
|
|
|
|
if index >= len(attr):
|
|
return True
|
|
|
|
return True if attr[index] else False
|
|
|
|
|
|
def filter_draw_ui(self, attr, titel = True):
|
|
layout = self.layout
|
|
if titel:
|
|
split = layout.split(factor = 0.41)
|
|
split.label(text = 'Filter :')
|
|
split.label(text = 'W X Y Z')
|
|
|
|
box = layout.box()
|
|
row = box.row()
|
|
row.label(text = 'Location')
|
|
row.prop(attr, 'filter_location', text = '')
|
|
row = box.row()
|
|
row.label(text = 'Rotation')
|
|
row.prop(attr, 'filter_rotation', text = '')
|
|
row = box.row()
|
|
row.label(text = 'Scale')
|
|
row.prop(attr, 'filter_scale', text = '')
|
|
|
|
if hasattr(attr, 'filter_custom_props'):
|
|
row = layout.row(align = True)
|
|
row.alignment = 'CENTER'
|
|
row.label(text = 'Custom Properties ')
|
|
row.prop(attr, 'filter_custom_props', text = '')
|
|
|
|
if hasattr(attr, 'filter_keyframes'):
|
|
row = layout.row(align = True)
|
|
row.alignment = 'CENTER'
|
|
row.label(text = 'Selected Keyframes ')
|
|
row.prop(attr, 'filter_keyframes', text = '')
|
|
|
|
|
|
class FilterUI(bpy.types.Operator):
|
|
"""Filter Location Rotation and Scale Properties"""
|
|
bl_idname = "fcurves.filter_ui"
|
|
bl_label = "Filter W X Y Z"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def invoke(self, context, event):
|
|
wm = context.window_manager
|
|
return wm.invoke_popup(self, width = 150)
|
|
|
|
def draw(self, context):
|
|
filter_draw_ui(self, context.scene.animtoolbox)
|
|
|
|
def execute(self, context):
|
|
return {'CANCELLED'}
|
|
|
|
def filter_name_update(self, context):
|
|
'''The name displayed on the filter button'''
|
|
scene = context.scene
|
|
atb = scene.animtoolbox
|
|
atb.filter_name = ''
|
|
if not any(list(atb.filter_location) + list(atb.filter_rotation) + list(atb.filter_scale)):
|
|
return
|
|
|
|
#Write the name of the transforms and array that are being used
|
|
array_dict = {0 : 'x', 1 : 'y', 2 : 'z'}
|
|
array_rot_dict = {0 : 'w', 1 : 'x', 2 : 'y', 3 : 'z'}
|
|
for transform in ('location', 'rotation', 'scale'):
|
|
filter_attr = getattr(atb, 'filter_' + transform)
|
|
if not any(list(filter_attr)):
|
|
continue
|
|
if atb.filter_name != '':
|
|
atb.filter_name = atb.filter_name + ' / '
|
|
|
|
atb.filter_name += transform[0].upper() + ' ('
|
|
|
|
array_count = 0
|
|
for i in range(len(filter_attr)):
|
|
if not filter_attr[i]:
|
|
continue
|
|
if array_count:
|
|
atb.filter_name += '.'
|
|
|
|
if transform == 'rotation':
|
|
atb.filter_name += array_rot_dict[i]
|
|
else:
|
|
atb.filter_name += array_dict[i]
|
|
|
|
array_count +=1
|
|
# if i != len(filter_attr)-1:
|
|
# atb.filter_name += '.'
|
|
|
|
atb.filter_name += ')'
|
|
#remove the last point
|
|
#atb.filter_name = atb.filter_name[:-1]
|
|
redraw_areas(['VIEW_3D'])
|
|
|
|
def redraw_areas(areas):
|
|
for area in bpy.context.window.screen.areas:
|
|
if area.type in areas:
|
|
area.tag_redraw()
|
|
|
|
|
|
def draw_func(self, context):
|
|
layout = self.layout
|
|
|
|
# Append your label to the frame range template ID
|
|
layout.operator("anim.markers_framerange", icon = 'MARKER', text ='', depress = context.scene.animtoolbox.marker_frame_range)
|
|
|
|
def get_obj_slot(obj, action):
|
|
'''Get the slot in the action that this object is using either it's object, or shapekeys'''
|
|
|
|
if not hasattr(action, 'slots'):
|
|
return None
|
|
|
|
for slot in action.slots:
|
|
if obj in slot.users():
|
|
return slot
|
|
|
|
return None
|
|
|
|
def get_all_fcurves(action):
|
|
'''Get all the fcurves of an action'''
|
|
if not hasattr(action, 'layers'):
|
|
yield from action.fcurves
|
|
for layer in action.layers:
|
|
for strip in layer.strips:
|
|
for channelbag in strip.channelbags:
|
|
yield from channelbag.fcurves
|
|
|
|
def get_fcurves_channelbag(obj, action: bpy.types.Action):
|
|
|
|
if hasattr(action, 'layers'):
|
|
slot = get_obj_slot(obj, action)
|
|
if not slot:
|
|
return action.fcurves
|
|
channelbag = anim_utils.action_get_channelbag_for_slot(action, slot)
|
|
if channelbag:
|
|
return channelbag.fcurves
|
|
|
|
return action.fcurves
|
|
|
|
def add_channelbag(obj, action):
|
|
|
|
if not hasattr(action, 'layers'):
|
|
return
|
|
slot = get_obj_slot(obj, action)
|
|
|
|
if not len(action.layers):
|
|
layer = action.layers.new(obj.name)
|
|
else:
|
|
layer = action.layers[0]
|
|
|
|
if not len(layer.strips):
|
|
strip = layer.strips.new()
|
|
else:
|
|
strip = layer.strips[0]
|
|
|
|
if not len(strip.channelbags):
|
|
channelbag = strip.channelbags.new(slot)
|
|
else:
|
|
channelbag = anim_utils.action_get_channelbag_for_slot(action, slot)
|
|
|
|
return channelbag
|
|
|
|
def get_fcurves_container(obj: bpy.types.Object, action: bpy.types.Action):
|
|
'''Getting the container of the fcurves, either the action or channelbag
|
|
Using this when adding a new group to the action'''
|
|
|
|
if hasattr(action, 'layers'):
|
|
slot = get_obj_slot(obj, action)
|
|
if not slot:
|
|
return action
|
|
channelbag = anim_utils.action_get_channelbag_for_slot(action, slot)
|
|
return channelbag
|
|
|
|
else:
|
|
return action
|
|
|
|
def add_group_to_fcurve(obj, fcu, groupname):
|
|
'''Add an fcurve group based on the fcurve container, either action or channelbag'''
|
|
|
|
action = fcu.id_data
|
|
#get the container which is either a channelbag or a group
|
|
fcu_container = get_fcurves_container(obj, action)
|
|
group = fcu_container.groups.get(groupname)
|
|
if group is None:
|
|
group = fcu_container.groups.new(groupname)
|
|
|
|
fcu.group = group
|
|
|
|
return group
|
|
|
|
def add_slot_to_animdata(anim_data):
|
|
'''assign a slot to the object of anim data'''
|
|
if not hasattr(anim_data, 'action_slot'):
|
|
return None
|
|
slot = add_action_slot(anim_data.id_data, anim_data.action)
|
|
anim_data.action_slot = slot
|
|
|
|
return slot
|
|
|
|
def add_action_slot(obj, action):
|
|
'''Adding a new slot or finding available to an action, Relevant only for Blender 4.4 +'''
|
|
|
|
if not action:
|
|
return None
|
|
if not hasattr(action, 'layers'):
|
|
return None
|
|
|
|
if action.slots:
|
|
for slot in action.slots:
|
|
if obj in slot.users():
|
|
return slot
|
|
|
|
slot = action.slots.new(obj.id_type, obj.name)
|
|
return slot
|
|
|
|
def remove_empty_slots(action):
|
|
'''removing empty slots without users, using when extracting from a layer'''
|
|
if not action:
|
|
return
|
|
if not hasattr(action, 'layers'):
|
|
return
|
|
if not action.slots:
|
|
return
|
|
|
|
for slot in action.slots:
|
|
if not len(slot.users()):
|
|
action.slots.remove(slot)
|
|
|
|
classes = (ApplyKeyframesOffset, SelectKeyframesOffset, CopyMatrix, PasteMatrix, CopyRelativeMatrix, PasteRelativeMatrix, FilterUI,
|
|
ShareKeys, Markers_FrameRange, Markers_BakeRange, Markers_Retimer, BlendToMirroModal, InbetweenWorldMatrix, ConvertRotationMode, FindRotationMode,
|
|
Keyframe_Offset, RelativeCursor)
|
|
|
|
def register():
|
|
from bpy.utils import register_class
|
|
for cls in classes:
|
|
register_class(cls)
|
|
# bpy.types.Scene.marker_frame_range = bpy.props.BoolProperty(name = "Marker Frame Range", description = "Flag when marker frame range turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
|
bpy.types.DOPESHEET_HT_header.append(draw_func)
|
|
|
|
def unregister():
|
|
from bpy.utils import unregister_class
|
|
for cls in classes:
|
|
unregister_class(cls)
|
|
#del bpy.types.Scene.marker_frame_range
|
|
bpy.types.DOPESHEET_HT_header.remove(draw_func)
|