From b9831835047a6b2a8837b0486b18536a9b353d0b Mon Sep 17 00:00:00 2001 From: Pullusb Date: Tue, 12 Apr 2022 19:24:47 +0200 Subject: [PATCH] action swap and improved object / curve switch 0.8.0 - Easy jump to previous action - improved armature <-> curve back and forth - better UI --- CHANGELOG.md | 6 ++++ OP_expand_cycle_step.py | 66 +++++++++++++++++++++++++++++++++++++++-- OP_setup_curve_path.py | 59 ++++++++++++++++++++++-------------- __init__.py | 2 +- panels.py | 40 +++++++++++++++++-------- 5 files changed, 134 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7066bda..948e063 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +0.8.0 + +- Easy jump to previous action +- improved armature <-> curve back and forth +- better UI + 0.7.1 - customizable panel category name diff --git a/OP_expand_cycle_step.py b/OP_expand_cycle_step.py index 315c2b2..3d482dd 100644 --- a/OP_expand_cycle_step.py +++ b/OP_expand_cycle_step.py @@ -388,8 +388,6 @@ def pin_down_feets(): tmp_col.objects.unlink(obj) bpy.data.collections.remove(tmp_col) - - class UAC_OT_pin_feets(bpy.types.Operator): bl_idname = "anim.pin_feets" @@ -411,9 +409,73 @@ class UAC_OT_pin_feets(bpy.types.Operator): return {"FINISHED"} + +class UAC_OT_set_action(bpy.types.Operator): + bl_idname = "uac.set_action" + bl_label = "Set action by name" + bl_description = "Set action on active object using passed name" + bl_options = {"REGISTER", "INTERNAL"} + + act_name : bpy.props.StringProperty(options={'SKIP_SAVE'}) + + def execute(self, context): + act = bpy.data.actions.get(self.act_name) + if not act: + self.report({'ERROR'}, f'Could not find action {self.act_name} in bpy.data.actions') + return {"CANCELLED"} + + context.object.animation_data.action = act + return {"FINISHED"} + +class UAC_OT_step_back_actions(bpy.types.Operator): + bl_idname = "uac.step_back_actions" + bl_label = "Actions Step Back" + bl_description = "Step back to a previous action when 'baked' or 'pinned' action are not ok" + bl_options = {"REGISTER", "INTERNAL", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'ARMATURE' + + def invoke(self, context, event): + act = context.object.animation_data.action + base_name = act.name.replace('_baked', '').replace('_pinned', '') + base_name = re.sub(r'\.\d{3}', '', base_name) # remove duplicate to search everything that has the same base + + # self.actions = [a for a in bpy.data.actions if a.name.startswith(base_name) and not a.name == act.name] # skip current action + self.actions = [a for a in bpy.data.actions if a.name.startswith(base_name)] + + if not len(self.actions): + self.report({'ERROR'}, f'no other action found for {act.name}\nUsing basename{base_name}') + return {'CANCELLED'} + + if len(self.actions) == 1: + context.object.animation_data.action = self.actions[0] + return self.execute(context) + + self.actions.sort(key=lambda x: len(x.name)) + return context.window_manager.invoke_props_popup(self, event) + + + def draw(self, context): + layout = self.layout + # layout.label(text=f'Current Action: {context.object.animation_data.action.name}') + layout.label(text='Actions with same name base:') + for a in self.actions: + if a == context.object.animation_data.action: + layout.label(text=f'(current) >> {a.name}', icon='ACTION') + continue + layout.operator('UAC_OT_set_action', text=a.name, icon='ACTION').act_name = a.name + + def execute(self, context): + return {"FINISHED"} + + classes=( UAC_OT_bake_cycle_and_step, UAC_OT_pin_feets, +UAC_OT_set_action, +UAC_OT_step_back_actions, ) def register(): diff --git a/OP_setup_curve_path.py b/OP_setup_curve_path.py index 6b7c162..1fe441c 100644 --- a/OP_setup_curve_path.py +++ b/OP_setup_curve_path.py @@ -226,18 +226,33 @@ class UAC_OT_edit_curve(bpy.types.Operator): context.view_layer.objects.active = curve bpy.ops.object.mode_set(mode='EDIT', toggle=False) # EDIT_CURVE + b.id_data.select_set(False) return {"FINISHED"} -class UAC_OT_set_choice_id(bpy.types.Operator): - bl_idname = "uac.set_choice_id" - bl_label = "Chosen ID" - bl_description = "Set passed id to a custom prop in window manager" +class UAC_OT_go_to_object(bpy.types.Operator): + bl_idname = "uac.go_to_object" + bl_label = "Go To Object" + bl_description = "Go to object in pose mode" bl_options = {"REGISTER", "INTERNAL"} - idx : bpy.props.IntProperty(default=0, options={'SKIP_SAVE'}) + obj_name : bpy.props.StringProperty(options={'SKIP_SAVE'}) def execute(self, context): - context.window_manager['back_to_armature_idx_prop'] = self.idx + obj = context.scene.objects.get(self.obj_name) + if not obj: + self.report({'ERROR'}, f'Could not find object {self.obj_name} in scene objects') + return {"CANCELLED"} + bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + ## deselect all armatures (or all objects) + for ob in context.scene.objects: + if ob.type == 'ARMATURE': + ob.select_set(False) + + obj.select_set(True) + context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='POSE', toggle=False) + + self.report({'INFO'}, f'Back to pose mode {obj.name}') return {"FINISHED"} class UAC_OT_object_from_curve(bpy.types.Operator): @@ -266,31 +281,29 @@ class UAC_OT_object_from_curve(bpy.types.Operator): self.report({'ERROR'}, 'No armature using this curve found') return {"CANCELLED"} + curve.select_set(False) if len(self.armatures) > 1: - # context.window_manager['back_to_armature_idx_prop'] - # return context.window_manager.invoke_props_dialog(self, width=450) # execute on ok return context.window_manager.invoke_props_popup(self, event) # execute on change + # set pose mode on only object available + obj, pb = self.armatures[0] + bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + for ob in context.scene.objects: + if ob.type == 'ARMATURE': + ob.select_set(False) + obj.select_set(True) + context.view_layer.objects.active = obj + bpy.ops.object.mode_set(mode='POSE', toggle=False) + self.report({'INFO'}, f'Back to pose mode {obj.name} (constraint on {pb.name})') + return self.execute(context) def draw(self, context): layout = self.layout - for i, item in enumerate(self.armatures): - arm, pb = item - layout.operator('uac.set_choice_id', text=f'{arm.name} > {pb.name}', icon='OUTLINER_OB_ARMATURE').idx = i + for arm, pb in self.armatures: + layout.operator('uac.go_to_object', text=f'{arm.name} ({pb.name})', icon='OUTLINER_OB_ARMATURE').obj_name = arm.name def execute(self, context): - if len(self.armatures) > 1: - # use user chosen index - obj, pb = self.armatures[context.window_manager['back_to_armature_idx_prop']] - else: - obj, pb = self.armatures[0] - - bpy.ops.object.mode_set(mode='OBJECT', toggle=False) - obj.select_set(True) - context.view_layer.objects.active = obj - bpy.ops.object.mode_set(mode='POSE', toggle=False) - self.report({'INFO'}, f'Back to pose mode {obj.name} (constraint on {pb.name})') return {"FINISHED"} classes=( @@ -298,7 +311,7 @@ UAC_OT_create_curve_path, UAC_OT_create_follow_path, UAC_OT_snap_curve_to_ground, UAC_OT_edit_curve, -UAC_OT_set_choice_id, +UAC_OT_go_to_object, UAC_OT_object_from_curve, # use set_choice_id is used to set an index in object_from_curve pop up menu ) diff --git a/__init__.py b/__init__.py index eba20f4..7bb5846 100644 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "Unfold Anim Cycle", "description": "Anim tools to develop walk/run cycles along a curve", "author": "Samuel Bernou", - "version": (0, 7, 1), + "version": (0, 8, 0), "blender": (3, 0, 0), "location": "View3D", "warning": "WIP", diff --git a/panels.py b/panels.py index 884adb9..6aa1509 100644 --- a/panels.py +++ b/panels.py @@ -23,10 +23,27 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): row.label(text='Forward Axis') row.prop(settings, "forward_axis", text='') layout.operator("uac.autoset_axis", text='Auto-Set Axis') + + pb = None + constrained = False + if ob and ob.type == 'ARMATURE': + pb = ob.pose.bones.get(prefs.tgt_bone) + if pb: + follow = pb.constraints.get('Follow Path') + if follow and follow.target: + constrained = True + + if not settings.path_to_follow and not constrained: + layout.operator('anim.create_curve_path', text='Create Curve at Root Position', icon='CURVE_BEZCURVE') + else: + layout.operator('uac.edit_curve', text='Edit Curve', icon='OUTLINER_DATA_CURVE') # FORCE_CURVE - if not settings.path_to_follow: - layout.operator('anim.create_curve_path', text='Create Curve at Root Position', icon='CURVE_BEZCURVE') - + elif ob and ob.type == 'CURVE': + if context.mode in ('OBJECT', 'EDIT_CURVE') \ + and settings.path_to_follow \ + and ob == settings.path_to_follow: + layout.operator('uac.object_from_curve', text='Back To Object', icon='LOOP_BACK') + box = layout.box() expand_icon = 'TRIA_DOWN' if tweak else 'TRIA_RIGHT' box.prop(settings, 'tweak', text='Curve Options', icon=expand_icon) @@ -41,7 +58,7 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): # Determine if already has a constraint (a bit too much condition in a panel...) - constrained = False + if ob: if ob.type == 'ARMATURE': pb = ob.pose.bones.get(prefs.tgt_bone) @@ -54,13 +71,7 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): # if context.mode == 'POSE': if not constrained: box.operator('anim.create_follow_path', text='Add follow path constraint', icon='CON_FOLLOWPATH') - box.operator('uac.edit_curve', text='Edit Curve', icon='OUTLINER_DATA_CURVE') # FORCE_CURVE - elif ob.type == 'CURVE': - if context.mode in ('OBJECT', 'EDIT_CURVE') \ - and settings.path_to_follow \ - and ob == settings.path_to_follow: - box.operator('uac.object_from_curve', text='Back To Object', icon='LOOP_BACK') box = layout.box() col=box.column() @@ -75,7 +86,7 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): ## Bake cycle (on selected) box = layout.box() col=box.column() - col.label(text='Keys:') + col.label(text='Actions:') row=col.row() row.prop(settings, "linear", text='Linear') row.prop(settings, "expand_on_selected_bones") @@ -87,8 +98,11 @@ class UAC_PT_walk_cycle_anim_panel(bpy.types.Panel): col.operator('anim.pin_feets', text='Pin feets', icon='PINNED') ## show a dropdown allowing to go back to unpinned, unbaked version of the animation - # if ob.type == 'ARMATURE': - # pass + if ob and ob.type == 'ARMATURE': + if ob.animation_data and ob.animation_data.action: + if 'baked' in ob.animation_data.action.name or 'pinned' in ob.animation_data.action.name: + col.operator('uac.step_back_actions', text='Use Previous Actions', icon= 'ACTION') +