From 336d1b264cb928105d5335343729f47fdb713ab7 Mon Sep 17 00:00:00 2001 From: Pullusb Date: Fri, 4 Nov 2022 18:45:37 +0100 Subject: [PATCH] initial code for follow curve --- OP_follow_curve.py | 68 +++++++++++++++++++++ UI_tools.py | 4 ++ __init__.py | 8 ++- utils.py | 145 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 OP_follow_curve.py diff --git a/OP_follow_curve.py b/OP_follow_curve.py new file mode 100644 index 0000000..8cad9a8 --- /dev/null +++ b/OP_follow_curve.py @@ -0,0 +1,68 @@ +import bpy +from mathutils import Vector +from . import utils + + +class GPTB_OT_create_follow_path_curve(bpy.types.Operator): + bl_idname = "object.create_follow_path_curve" + bl_label = "Create Follow Path Curve" + bl_description = "Create curve and add follow path constraint\ + \n(remove location offset from object if any)" + bl_options = {"REGISTER", "UNDO"} + + @classmethod + def poll(cls, context): + return context.object + + def execute(self, context): + ob = context.object + # settings = context.scene.anim_cycle_settings + bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + + ## For bones + # root_name = fn.get_root_name(context=context) + # root = ob.pose.bones.get(root_name) + # if not root: + # self.report({'ERROR'}, f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name') + # return {"CANCELLED"} + + ## create curve at bone position + # loc = ob.matrix_world @ root.matrix.to_translation() + # root_axis_vec = fn.get_direction_vector_from_enum(settings.forward_axis) + ## get real world direction of the root + # world_forward = (root.matrix @ root_axis_vec) - root.matrix.to_translation() + + loc = ob.matrix_world.to_translation() + + ## X global + # TODO: Set direction orientation in view space (UP, LEFT, RIGHT, DOWN) + direction = Vector((1,0,0)) + curve = utils.create_curve(location=loc, + direction=direction.normalized() * 2, + name='curve_path', + context=context) + + utils.create_follow_path_constraint(ob, curve) + + ## reset location to remove offset + ob.location = (0,0,0) + # ob.keyframe_insert('location') + ob.rotation_euler = (0,0,0) + # ob.keyframe_insert('rotation_euler') + + # refresh evaluation so constraint shows up correctly + bpy.context.scene.frame_set(bpy.context.scene.frame_current) + return {"FINISHED"} + + +classes = ( + GPTB_OT_create_follow_path_curve, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) \ No newline at end of file diff --git a/UI_tools.py b/UI_tools.py index 1287fa2..d3e3a16 100644 --- a/UI_tools.py +++ b/UI_tools.py @@ -222,6 +222,10 @@ class GPTB_PT_anim_manager(Panel): row.operator('gp.toggle_hide_gp_modifier', text = 'ON').show = True row.operator('gp.toggle_hide_gp_modifier', text = 'OFF').show = False + ## Follow curve path + row = col.row(align=True) + row.operator('object.create_follow_path_curve', text='Create Curve', icon='CURVE_BEZCURVE') + ## This can go in an extra category... col = layout.column() col.use_property_split = False diff --git a/__init__.py b/__init__.py index 7bf2f98..a03e596 100755 --- a/__init__.py +++ b/__init__.py @@ -4,7 +4,7 @@ bl_info = { "name": "GP toolbox", "description": "Tool set for Grease Pencil in animation production", "author": "Samuel Bernou, Christophe Seux", -"version": (2, 0, 11), +"version": (2, 1, 0), "blender": (3, 0, 0), "location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties", "warning": "", @@ -45,6 +45,7 @@ from . import OP_material_picker from . import OP_git_update from . import OP_layer_namespace from . import OP_pseudo_tint +from . import OP_follow_curve # from . import OP_eraser_brush # from . import TOOL_eraser_brush from . import handler_draw_cam @@ -658,8 +659,8 @@ def set_namespace_env(name_env, prop_group): if tag_list: tag_list = tag_list.strip(',').split(',') current_pfix = [n.tag for n in prop_group.namespaces if n.tag] - for n in prop_group.namespaces: - print(n.tag, n.name) + # for n in prop_group.namespaces: + # print(n.tag, n.name) for p in tag_list: tag = p.split(':')[0].strip() name = '' if not ':' in p else p.split(':')[1].strip() @@ -789,6 +790,7 @@ addon_modules = ( OP_git_update, OP_layer_picker, OP_layer_nav, + OP_follow_curve, # OP_eraser_brush, # TOOL_eraser_brush, # experimental eraser brush handler_draw_cam, diff --git a/utils.py b/utils.py index 4bea059..0c37397 100644 --- a/utils.py +++ b/utils.py @@ -956,3 +956,148 @@ def iterate_selector(zone, attr, state, info_attr = None, active_access='active' info = getattr(active_item, info_attr) return info, bottom + +# ----------------- +### Curve handle +# ----------------- + +def create_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None): + '''Create curve at provided location and direction vector''' + + ## option to create nurbs instaed of bezier ? + + context = context or bpy.context + + ## using ops (dirty) + # bpy.ops.curve.primitive_bezier_curve_add(radius=1, enter_editmode=enter_edit, align='WORLD', location=location, scale=(1, 1, 1)) + # curve = context.object + # curve.name = 'curve_path' + # # fast straighten + # bpy.ops.curve.handle_type_set(type='VECTOR') + # bpy.ops.curve.handle_type_set(type='ALIGNED') + # bpy.ops.transform.translate(value=(1, 0, 0), orient_type='LOCAL', + # orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='LOCAL', + # constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False) + + ## using data + curve_data = bpy.data.curves.new(name, 'CURVE') # ('CURVE', 'SURFACE', 'FONT') + curve_data.dimensions = '3D' + curve_data.use_path = True + + curve = bpy.data.objects.new(name, curve_data) + spl = curve_data.splines.new('BEZIER') # ('POLY', 'BEZIER', 'NURBS') + spl.bezier_points.add(1) # One point already exists + for i in range(2): + spl.bezier_points[i].handle_left_type = 'VECTOR' # ('FREE', 'VECTOR', 'ALIGNED', 'AUTO') + spl.bezier_points[i].handle_right_type = 'VECTOR' + spl.bezier_points[1].co = direction + + # Back to aligned mode + for i in range(2): + spl.bezier_points[i].handle_right_type = spl.bezier_points[i].handle_left_type = 'ALIGNED' + + # Select second point + spl.bezier_points[1].select_control_point = True + spl.bezier_points[1].select_left_handle = True + spl.bezier_points[1].select_right_handle = True + + # link + context.scene.collection.objects.link(curve) + + # curve object settings + curve.location = location + curve.show_in_front = True + + # enter edit + if enter_edit and context.mode == 'OBJECT': + curve.select_set(True) + context.view_layer.objects.active = curve + bpy.ops.object.mode_set(mode='EDIT', toggle=False) # EDIT_CURVE + + ## set viewport overlay visibility for better view + if context.space_data.type == 'VIEW_3D': + context.space_data.overlay.show_curve_normals = True + context.space_data.overlay.normals_length = 0.2 + + return curve + +def get_direction_vector_from_enum(string) -> Vector: + orient_vectors = { + 'FORWARD_X' : Vector((1,0,0)), + 'FORWARD_Y' : Vector((0,1,0)), + 'FORWARD_Z' : Vector((0,0,1)), + 'TRACK_NEGATIVE_X' : Vector((-1,0,0)), + 'TRACK_NEGATIVE_Y' : Vector((0,-1,0)), + 'TRACK_NEGATIVE_Z' : Vector((0,0,-1)) + } + return orient_vectors[string] + +def orentation_track_from_vector(input_vector) -> str: + '''return closest world track orientation name from passed vector direction''' + orient_vectors = { + 'FORWARD_X' : Vector((1,0,0)), + 'FORWARD_Y' : Vector((0,1,0)), + 'FORWARD_Z' : Vector((0,0,1)), + 'TRACK_NEGATIVE_X' : Vector((-1,0,0)), + 'TRACK_NEGATIVE_Y' : Vector((0,-1,0)), + 'TRACK_NEGATIVE_Z' : Vector((0,0,-1)) + } + + orient = None + min_angle = 10000 + for track, v in orient_vectors.items(): + angle = input_vector.angle(v) + if angle < min_angle: + min_angle = angle + orient = track + + return orient + +def create_follow_path_constraint(ob, curve, follow_curve=False): + '''return create constraint''' + # # Clear bone follow path constraint + exiting_fp_constraints = [c for c in ob.constraints if c.type == 'FOLLOW_PATH'] + for c in exiting_fp_constraints: + ob.constraints.remove(c) + + # loc = ob.matrix_world @ ob.matrix.to_translation() + if ob.location != (0,0,0): + old_loc = ob.location + ob.location = (0,0,0) + print(f'ob moved from {old_loc} to (0,0,0) to counter follow curve offset') + + const = ob.constraints.new('FOLLOW_PATH') + const.target = curve + if follow_curve: + const.use_curve_follow = True + return const + + ## on_bones: + # prefs = get_addon_prefs() + # root_name = prefs.tgt_bone + # root = ob.pose.bones.get(root_name) + + # if not root: + # return ('ERROR', f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name') + + # # Clear bone follow path constraint + # exiting_fp_constraints = [c for c in root.constraints if c.type == 'FOLLOW_PATH'] + # for c in exiting_fp_constraints: + # root.constraints.remove(c) + + # # loc = ob.matrix_world @ root.matrix.to_translation() + # if root.name == ('world', 'root') and root.location != (0,0,0): + # old_loc = root.location + # root.location = (0,0,0) + # print(f'root moved from {old_loc} to (0,0,0) to counter follow curve offset') + + # const = root.constraints.new('FOLLOW_PATH') + # const.target = curve + # # axis only in this case, should be in addon to prefs + + # ## determine which axis to use... maybe found orientation in world space from matrix_basis ? + # root_world_base_direction = root.bone.matrix_local @ get_direction_vector_from_enum(bpy.context.scene.anim_cycle_settings.forward_axis) + # const.forward_axis = orentation_track_from_vector(root_world_base_direction) # 'TRACK_NEGATIVE_Y' # bpy.context.scene.anim_cycle_settings.forward_axis # 'FORWARD_X' + # print('const.forward_axis: ', const.forward_axis) + # const.use_curve_follow = True + # return curve, const \ No newline at end of file