gp_toolbox/OP_breakdowner.py

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()