From a021535b3ff71f99e9d984807418fea913f347d4 Mon Sep 17 00:00:00 2001 From: Pullusb Date: Wed, 9 Nov 2022 18:59:22 +0100 Subject: [PATCH] Add curve follow path creation and management 2.1.0 - added: 3 actions buttons: - create curve with follow path and go into curve edit - go back to object - got to curve edit (if follow path constraint exists with a curve target) - if follow path exists, button to remove constraint --- CHANGELOG.md | 8 +++ OP_follow_curve.py | 147 +++++++++++++++++++++++++++++++++++++++++++++ OP_helpers.py | 2 +- UI_tools.py | 14 ++++- 4 files changed, 169 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4223dc8..8b3dba8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +2.1.0 + +- added: 3 actions buttons: + - create curve with follow path and go into curve edit + - go back to object + - got to curve edit (if follow path constraint exists with a curve target) + - if follow path exists, button to remove constraint + 2.0.11 - fix: prefix set by project environment diff --git a/OP_follow_curve.py b/OP_follow_curve.py index 8cad9a8..bdb5f0e 100644 --- a/OP_follow_curve.py +++ b/OP_follow_curve.py @@ -54,9 +54,156 @@ class GPTB_OT_create_follow_path_curve(bpy.types.Operator): bpy.context.scene.frame_set(bpy.context.scene.frame_current) return {"FINISHED"} +class GPTB_OT_edit_curve(bpy.types.Operator): + bl_idname = "object.edit_curve" + bl_label = "Edit Curve" + bl_description = "Edit curve used as follow path constraint" + bl_options = {"REGISTER", "INTERNAL", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object + + def execute(self, context): + ob = context.object + curve = next((c.target for c in ob.constraints if c.type == 'FOLLOW_PATH' and c.target), None) + + if curve is None: + self.report({"ERROR"}, 'No follow path curve found') + return {"CANCELLED"} + + # Object mode, set curve as active, go Edit + utils.go_edit_mode(curve) + # curve context.mode -> EDIT_CURVE + # b.id_data.select_set(False) + ob.select_set(False) + return {"FINISHED"} + +class GPTB_OT_remove_follow_path(bpy.types.Operator): + bl_idname = "object.remove_follow_path" + bl_label = "Remove Follow Path Constraint" + bl_description = "Remove follow path on object" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object + + def execute(self, context): + ob = context.object + const = next((c for c in ob.constraints if c.type == 'FOLLOW_PATH'), None) + if not const: + self.report({'ERROR'}, f'No follow path constraint on "{ob.name}" found') + return {"CANCELLED"} + + # store position + mat = ob.matrix_world.copy() + + ob.constraints.remove(const) + + # restore position + ob.matrix_world = mat + + self.report({'INFO'}, f'Removed follow_path constraint on "{ob.name}"') + # Also remove offset action ? maybe give the choice + return {"FINISHED"} + +class GPTB_OT_go_to_object(bpy.types.Operator): + bl_idname = "object.go_to_object" + bl_label = "Go To Object" + bl_description = "Go to object in pose mode" + bl_options = {"REGISTER", "INTERNAL"} + + obj_name : bpy.props.StringProperty(options={'SKIP_SAVE'}) + + def execute(self, context): + 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) + + for ob in context.scene.objects: + ob.select_set(False) + + # Set active + obj.select_set(True) + context.view_layer.objects.active = obj + + if obj.type == 'ARMATURE': + bpy.ops.object.mode_set(mode='POSE', toggle=False) + self.report({'INFO'}, f'Back to pose mode, {obj.name}') + + elif obj.type == 'GPENCIL': + bpy.ops.object.mode_set(mode='PAINT_GPENCIL', toggle=False) + + else: + self.report({'INFO'}, f'Back to object mode, {obj.name}') + + return {"FINISHED"} + +class GPTB_OT_object_from_curve(bpy.types.Operator): + bl_idname = "object.object_from_curve" + bl_label = "Back To Following Object" + bl_description = "Go on following object from current curve" + bl_options = {"REGISTER", "INTERNAL", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'CURVE' + + def invoke(self, context, event): + curve = context.object + self.objects = [] + for o in context.scene.objects: + + if o.type != 'ARMATURE': + for c in o.constraints: + if c.type == 'FOLLOW_PATH' and c.target and c.target == curve: + self.objects.append(o) + else: + for pb in o.pose.bones: + for c in pb.constraints: + if c.type == 'FOLLOW_PATH' and c.target and c.target == curve: + self.objects.append(o) + break + + if not self.objects: + self.report({'ERROR'}, 'No object following current curve found') + return {"CANCELLED"} + + curve.select_set(False) + if len(self.objects) > 1: + return context.window_manager.invoke_props_popup(self, event) # execute on change + + # set pose mode on only object available + obj = self.objects[0] + bpy.ops.object.go_to_object(obj_name=obj.name) + # bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + # for ob in context.scene.objects: + # 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 obj in self.objects: + layout.operator('object.go_to_object', text=obj.name, icon='OBJECT_DATA').obj_name = obj.name + + def execute(self, context): + return {"FINISHED"} classes = ( GPTB_OT_create_follow_path_curve, + GPTB_OT_edit_curve, + GPTB_OT_remove_follow_path, + GPTB_OT_go_to_object, + GPTB_OT_object_from_curve, ) def register(): diff --git a/OP_helpers.py b/OP_helpers.py index 955aa12..111beee 100644 --- a/OP_helpers.py +++ b/OP_helpers.py @@ -398,7 +398,7 @@ class GPTB_OT_toggle_mute_animation(bpy.types.Operator): if self.mode == 'CAMERA' and o.type != 'CAMERA': continue - # mute attribute aniamtion for GP and cameras + # mute attribute animation for GP and cameras if o.type in ('GPENCIL', 'CAMERA') and o.data.animation_data: gp_act = o.data.animation_data.action if gp_act: diff --git a/UI_tools.py b/UI_tools.py index d3e3a16..c273c38 100644 --- a/UI_tools.py +++ b/UI_tools.py @@ -224,7 +224,19 @@ class GPTB_PT_anim_manager(Panel): ## Follow curve path row = col.row(align=True) - row.operator('object.create_follow_path_curve', text='Create Curve', icon='CURVE_BEZCURVE') + # row.operator('object.create_follow_path_curve', text='Create Curve', icon='CURVE_BEZCURVE') + + if context.object and context.object.type == 'CURVE' and context.mode in ('OBJECT', 'EDIT_CURVE'): + row.operator('object.object_from_curve', text='Back To Object', icon='LOOP_BACK') + + elif (follow_const := context.object.constraints.get('Follow Path')) and follow_const.target: + row.operator('object.edit_curve', text='Edit Curve', icon='OUTLINER_DATA_CURVE') + row.operator('object.remove_follow_path', text='', icon='X') + col.label(text=f'{context.object.name} -> {follow_const.target.name}', icon='CON_FOLLOWPATH') + + else: + col.operator('object.create_follow_path_curve', text='Create Curve', icon='CURVE_BEZCURVE') + ## This can go in an extra category... col = layout.column()