347 lines
14 KiB
Python
347 lines
14 KiB
Python
#Breakdowner object mode V1
|
|
import bpy
|
|
import re
|
|
from mathutils import Vector, Matrix
|
|
from math import radians, degrees
|
|
|
|
# exemple for future improve: https://justinsbarrett.com/tweenmachine/
|
|
|
|
def get_surrounding_points(fc, frame):
|
|
'''Take an Fcurve and a frame and return previous and next frames'''
|
|
if not frame: frame = bpy.context.scene.frame_current
|
|
p_pt = n_pt = None
|
|
mins = []
|
|
maxs = []
|
|
for pt in fc.keyframe_points:
|
|
if pt.co[0] < frame:
|
|
p_pt = pt
|
|
if pt.co[0] > frame:
|
|
n_pt = pt
|
|
break
|
|
|
|
return p_pt, n_pt
|
|
|
|
## unused direct breackdown func
|
|
def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale'), axe=(0,1,2)):
|
|
cf = bpy.context.scene.frame_current# use operator context (may be unsynced timeline)
|
|
axes_name = ('x', 'y', 'z')
|
|
obj = bpy.context.object# better use self.context
|
|
if not obj:
|
|
print('no active object')
|
|
return
|
|
|
|
anim_data = obj.animation_data
|
|
if not anim_data:
|
|
print(f'no animation data on obj: {obj.name}')
|
|
return
|
|
|
|
action = anim_data.action
|
|
if not action:
|
|
print(f'no action on animation data of obj: {obj.name}')
|
|
return
|
|
|
|
skipping = []
|
|
|
|
for fc in action.fcurves:
|
|
# if fc.data_path.split('"')[1] in bone_names_filter:# bones
|
|
# if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones
|
|
if fc.data_path in channels and fc.array_index in axe:# .split('.')[-1]
|
|
fc_name = f'{fc.data_path}.{axes_name[fc.array_index]}'
|
|
print(fc_name)
|
|
pkf, nkf = get_surrounding_points(fc, frame=cf)
|
|
# check previous, next keyframe (if one or both is missing, skip)
|
|
if pkf is None or nkf is None:
|
|
skipping.append(fc_name)
|
|
continue
|
|
|
|
prv, nxt = pkf.co[1], nkf.co[1]
|
|
if prv == nxt:
|
|
nval = prv
|
|
else:
|
|
nval = ((percentage * (nxt - prv)) / 100) + prv#intermediate val
|
|
print('value:', nval)
|
|
|
|
fc.keyframe_points.add(1)
|
|
fc.keyframe_points[-1].co[0] = cf
|
|
fc.keyframe_points[-1].co[1] = nval
|
|
fc.keyframe_points[-1].type = pkf.type# make same type ?
|
|
fc.keyframe_points[-1].interpolation = pkf.interpolation
|
|
fc.update()
|
|
# obj.keyframe_insert(fc.data_path, index=fc.array_index, )
|
|
|
|
### breakdown_keys(channels=('location', 'rotation_euler', 'scale'))
|
|
|
|
class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
|
|
"""Breakdown percentage between two keyframes like bone pose mode"""
|
|
bl_idname = "object.breakdown_anim"
|
|
bl_label = "breakdown object keyframe"
|
|
bl_description = "Percentage value between previous dans next keyframes, "
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
pressed_ctrl = False
|
|
pressed_shift = False
|
|
# pressed_alt = False
|
|
str_val = ''
|
|
step = 5
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.mode == 'OBJECT'and context.object
|
|
|
|
def percentage(self):
|
|
return (self.xmouse - self.xmin) / self.width * 100
|
|
|
|
def assign_transforms(self, percentage):
|
|
for obj, path_dic in self.obdic.items():
|
|
for data_path, index_dic in path_dic.items():
|
|
for index, vals in index_dic.items():# prv, nxt = vals
|
|
# exec(f'bpy.data.objects["{obj.name}"].{data_path}[{index}] = {((self.percentage() * (vals[1] - vals[0])) / 100) + vals[0]}')
|
|
getattr(obj, data_path)[index] = ((percentage * (vals[1] - vals[0])) / 100) + vals[0]
|
|
|
|
def modal(self, context, event):
|
|
context.area.tag_redraw()
|
|
refresh = False
|
|
### /TESTER - keycode printer (flood console but usefull to know a keycode name)
|
|
# if event.type not in {'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE'}:#avoid flood of mouse move.
|
|
# print('key:', event.type, 'value:', event.value)
|
|
### TESTER/
|
|
|
|
## Handle modifier keys state
|
|
if event.type in {'LEFT_SHIFT', 'RIGHT_SHIFT'}: self.pressed_shift = event.value == 'PRESS'
|
|
if event.type in {'LEFT_CTRL', 'RIGHT_CTRL'}: self.pressed_ctrl = event.value == 'PRESS'
|
|
# if event.type in {'LEFT_ALT', 'RIGHT_ALT'}: self.pressed_alt = event.value == 'PRESS'
|
|
|
|
### KEYBOARD SINGLE PRESS
|
|
if event.value == 'PRESS':
|
|
refresh=True
|
|
if event.type in {'NUMPAD_MINUS'}:#, 'LEFT_BRACKET', 'WHEELDOWNMOUSE'
|
|
if self.str_val.startswith('-'):
|
|
self.str_val = self.str_val.strip('-')
|
|
else:
|
|
self.str_val = '-' + self.str_val#.strip('-')
|
|
|
|
## number
|
|
if event.type in {'ZERO', 'NUMPAD_0'}: self.str_val += '0'
|
|
if event.type in {'ONE', 'NUMPAD_1'}: self.str_val += '1'
|
|
if event.type in {'TWO', 'NUMPAD_2'}: self.str_val += '2'
|
|
if event.type in {'THREE', 'NUMPAD_3'}: self.str_val += '3'
|
|
if event.type in {'FOUR', 'NUMPAD_4'}: self.str_val += '4'
|
|
if event.type in {'FIVE', 'NUMPAD_5'}: self.str_val += '5'
|
|
if event.type in {'SIX', 'NUMPAD_6'}: self.str_val += '6'
|
|
if event.type in {'SEVEN', 'NUMPAD_7'}: self.str_val += '7'
|
|
if event.type in {'EIGHT', 'NUMPAD_8'}: self.str_val += '8'
|
|
if event.type in {'NINE', 'NUMPAD_9'}: self.str_val += '9'
|
|
|
|
if event.type in {'NUMPAD_PERIOD', 'COMMA'}:
|
|
if not '.' in self.str_val: self.str_val += '.'
|
|
|
|
# remove end chars
|
|
if event.type in {'DEL', 'BACK_SPACE'}: self.str_val = self.str_val[:-1]
|
|
|
|
# TODO lock transforms
|
|
# if event.type in {'G'}:pass# grab translate only
|
|
# if event.type in {'R'}:pass# rotation only
|
|
# if event.type in {'S'}:pass# scale only
|
|
|
|
## TODO need to check if self.str_val is valid and if not : display warning and return running modal
|
|
|
|
if re.search(r'\d', self.str_val):
|
|
use_num = True
|
|
percentage = float(self.str_val)
|
|
|
|
display_percentage = f'{percentage:.1f}' if '.' in self.str_val else f'{percentage:.0f}'
|
|
display_text = f'Breakdown: [{display_percentage}]% | manual type, erase for mouse control'
|
|
|
|
else:# use mouse
|
|
use_num = False
|
|
percentage = self.percentage()
|
|
if self.pressed_ctrl:# round
|
|
percentage = int(percentage)
|
|
if self.pressed_shift:# by step of 5
|
|
modulo = percentage % self.step
|
|
if modulo < self.step/2.0:
|
|
percentage = int( percentage - modulo )
|
|
else:
|
|
percentage = int( percentage + (self.step - modulo) )
|
|
|
|
display_percentage = f'{percentage:.1f}' if isinstance(percentage, float) else str(percentage)
|
|
display_text = f'Breakdown: {display_percentage}% | MODES ctrl: round - shift: 5 steps'
|
|
|
|
context.area.header_text_set(display_text)
|
|
|
|
## Get mouse move
|
|
if event.type in {'MOUSEMOVE'}:# , 'INBETWEEN_MOUSEMOVE'
|
|
if not use_num:#avoid compute on mouse move when manual type on
|
|
refresh = True
|
|
## percentage of xmouse in screen
|
|
self.xmouse = event.mouse_region_x
|
|
## assign stuff
|
|
|
|
if refresh:
|
|
self.assign_transforms(percentage)
|
|
|
|
|
|
# Valid
|
|
if event.type in {'RET', 'SPACE', 'LEFTMOUSE'}:
|
|
## 'INSERTKEY_AVAILABLE' ? ? filter
|
|
context.area.header_text_set(None)
|
|
context.window.cursor_set("DEFAULT")
|
|
|
|
if context.scene.tool_settings.use_keyframe_insert_auto:# auto key OK
|
|
if context.scene.tool_settings.use_keyframe_insert_keyingset and context.scene.keying_sets_all.active:
|
|
bpy.ops.anim.keyframe_insert('INVOKE_DEFAULT')#type='DEFAULT'
|
|
else:
|
|
bpy.ops.anim.keyframe_insert('INVOKE_DEFAULT', type='Available')
|
|
# "DEFAULT" not found in ('Available', 'Location', 'Rotation', 'Scaling', 'BUILTIN_KSI_LocRot', 'LocRotScale', 'BUILTIN_KSI_LocScale', 'BUILTIN_KSI_RotScale', 'BUILTIN_KSI_DeltaLocation', 'BUILTIN_KSI_DeltaRotation', 'BUILTIN_KSI_DeltaScale', 'BUILTIN_KSI_VisualLoc', 'BUILTIN_KSI_VisualRot', 'BUILTIN_KSI_VisualScaling', 'BUILTIN_KSI_VisualLocRot', 'BUILTIN_KSI_VisualLocRotScale', 'BUILTIN_KSI_VisualLocScale', 'BUILTIN_KSI_VisualRotScale')
|
|
return {'FINISHED'}
|
|
|
|
# Abort
|
|
if event.type in {'RIGHTMOUSE', 'ESC'}:
|
|
## Remove draw handler (if there was any)
|
|
# bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
|
|
|
context.scene.frame_set(self.cf)# reset object pos (update scene to re-evaluate anim)
|
|
context.area.header_text_set(None)#reset header
|
|
context.window.cursor_set("DEFAULT")
|
|
# print('Breakdown Cancelled')#Dbg
|
|
return {'CANCELLED'}
|
|
return {'RUNNING_MODAL'}
|
|
|
|
def invoke(self, context, event):
|
|
## cursors
|
|
## 'DEFAULT', 'NONE', 'WAIT', 'CROSSHAIR', 'MOVE_X', 'MOVE_Y', 'KNIFE', 'TEXT', 'PAINT_BRUSH', 'PAINT_CROSS', 'DOT', 'ERASER', 'HAND', 'SCROLL_X', 'SCROLL_Y', 'SCROLL_XY', 'EYEDROPPER'
|
|
## start checks
|
|
if context.area.type != 'VIEW_3D':
|
|
self.report({'WARNING'}, 'View3D not found, cannot run operator')
|
|
return {'CANCELLED'}
|
|
|
|
obj = bpy.context.object# better use self.context
|
|
if not obj:
|
|
self.report({'WARNING'}, 'No active object')
|
|
return {'CANCELLED'}
|
|
|
|
anim_data = obj.animation_data
|
|
if not anim_data:
|
|
self.report({'WARNING'}, f'No animation data on obj: {obj.name}')
|
|
return {'CANCELLED'}
|
|
|
|
action = anim_data.action
|
|
if not action:
|
|
self.report({'WARNING'}, f'No action on animation data of obj: {obj.name}')
|
|
return {'CANCELLED'}
|
|
|
|
## initiate variable to use
|
|
self.width = context.area.width# include sidebar...
|
|
## with exclude sidebar >>> C.screen.areas[3].regions[5].width
|
|
|
|
self.xmin = context.area.x
|
|
|
|
self.xmouse = event.mouse_region_x
|
|
self.pressed_alt = event.alt
|
|
self.pressed_ctrl = event.ctrl
|
|
self.pressed_shift = event.shift
|
|
|
|
self.cf = context.scene.frame_current
|
|
self.channels = ('location', 'rotation_euler', 'rotation_quaternion', 'scale')
|
|
|
|
skipping = []
|
|
found = 0
|
|
same = 0
|
|
|
|
self.obdic = {}
|
|
|
|
## TODO for ob in context.selected objects, need to reduce list with upper filters...
|
|
|
|
for fc in action.fcurves:
|
|
# if fc.data_path.split('"')[1] in bone_names_filter:# bones
|
|
# if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones
|
|
if fc.data_path in self.channels:# .split('.')[-1]# and fc.array_index in axe
|
|
fc_name = f'{fc.data_path}.{fc.array_index}'
|
|
pkf, nkf = get_surrounding_points(fc, frame = self.cf)
|
|
|
|
if pkf is None or nkf is None:
|
|
# check previous, next keyframe (if one or both is missing, skip)
|
|
skipping.append(fc_name)
|
|
continue
|
|
|
|
found +=1
|
|
prv, nxt = pkf.co[1], nkf.co[1]
|
|
|
|
if not obj in self.obdic:
|
|
self.obdic[obj] = {}
|
|
|
|
if not fc.data_path in self.obdic[obj]:
|
|
self.obdic[obj][fc.data_path] = {}
|
|
|
|
self.obdic[obj][fc.data_path][fc.array_index] = [prv, nxt]
|
|
|
|
if prv == nxt:
|
|
same += 1
|
|
else:
|
|
# exec(f'bpy.data.objects["{obj.name}"].{fc.data_path}[{fc.array_index}] = {((self.percentage() * (nxt - prv)) / 100) + prv}')
|
|
getattr(obj, fc.data_path)[fc.array_index] = ((self.percentage() * (nxt - prv)) / 100) + prv
|
|
|
|
'''# debug print value dic
|
|
import pprint
|
|
print('\nDIC print: ')
|
|
pprint.pprint(self.obdic)
|
|
'''
|
|
|
|
if not found:
|
|
self.report({'ERROR'}, "No key pairs to breakdown found ! need to be between a key pair")#
|
|
return {'CANCELLED'}
|
|
|
|
if found == same:
|
|
self.report({'ERROR'}, "All Key pairs found have same values")#
|
|
return {'CANCELLED'}
|
|
## Starts the modal
|
|
context.window.cursor_set("SCROLL_X")
|
|
context.window_manager.modal_handler_add(self)
|
|
return {'RUNNING_MODAL'}
|
|
|
|
|
|
### --- KEYMAP ---
|
|
|
|
addon_keymaps = []
|
|
def register_keymaps():
|
|
if bpy.app.background:
|
|
return
|
|
# pref = get_addon_prefs()
|
|
# if not pref.breakdowner_use_shortcut:
|
|
# return
|
|
|
|
addon = bpy.context.window_manager.keyconfigs.addon
|
|
|
|
try:
|
|
km = bpy.context.window_manager.keyconfigs.addon.keymaps["3D View"]
|
|
except Exception as e:
|
|
km = addon.keymaps.new(name = "3D View", space_type = "VIEW_3D")
|
|
pass
|
|
|
|
ops_id = 'object.breakdown_anim'
|
|
if ops_id not in km.keymap_items:
|
|
km = addon.keymaps.new(name='3D View', space_type='VIEW_3D')#EMPTY
|
|
kmi = km.keymap_items.new(ops_id, type="E", value="PRESS", shift=True)
|
|
addon_keymaps.append((km, kmi))
|
|
|
|
def unregister_keymaps():
|
|
if bpy.app.background:
|
|
return
|
|
for km, kmi in addon_keymaps:
|
|
km.keymap_items.remove(kmi)
|
|
|
|
addon_keymaps.clear()
|
|
|
|
### --- REGISTER ---
|
|
|
|
def register():
|
|
bpy.utils.register_class(OBJ_OT_breakdown_obj_anim)
|
|
register_keymaps()
|
|
|
|
def unregister():
|
|
unregister_keymaps()
|
|
bpy.utils.unregister_class(OBJ_OT_breakdown_obj_anim)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
register() |