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