diff --git a/__init__.py b/__init__.py index 3575888..bd11c1b 100755 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "gp interpolate", "author": "Christophe Seux, Samuel Bernou", - "version": (0, 7, 4), + "version": (0, 8, 0), "blender": (3, 6, 0), "location": "Sidebar > Gpencil Tab > Interpolate", "description": "Interpolate Grease pencil strokes over 3D", diff --git a/interpolate_strokes/__init__.py b/interpolate_strokes/__init__.py index 5e51979..0aadb12 100644 --- a/interpolate_strokes/__init__.py +++ b/interpolate_strokes/__init__.py @@ -1,13 +1,15 @@ from gp_interpolate.interpolate_strokes import (operators, properties, debug, + bind_points, #interpolate_simple ) modules = ( properties, operators, - debug + debug, + bind_points, #interpolate_simple, ) diff --git a/interpolate_strokes/bind_points.py b/interpolate_strokes/bind_points.py new file mode 100644 index 0000000..d491731 --- /dev/null +++ b/interpolate_strokes/bind_points.py @@ -0,0 +1,384 @@ +import bpy +import numpy as np +from time import perf_counter, time, sleep +from mathutils import Vector, Matrix + +from .. import utils + +from mathutils.geometry import (barycentric_transform, + intersect_point_tri, + intersect_point_line, + intersect_line_plane, + tessellate_polygon) + +import math +import gpu +from bpy_extras import view3d_utils +from gpu_extras.batch import batch_for_shader + + +def raycast_objects(context, event, dg): + """ + Execute ray cast + return object hit, hit world location, normal of hitted face and face index + """ + + region = context.region + rv3d = context.region_data + coord = event.mouse_region_x, event.mouse_region_y + + view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord) + ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord) + ray_target = ray_origin + view_vector + + def visible_objects_and_duplis():# -> Generator[tuple, Any, None]: + """Loop over (object, matrix) pairs (mesh only)""" + + # depsgraph = dg ## TRY to used passed depsgraph + depsgraph = context.evaluated_depsgraph_get() + for dup in depsgraph.object_instances: + if dup.is_instance: # Real dupli instance + obj = dup.instance_object + yield (obj, dup.matrix_world.copy()) + else: # Usual object + obj = dup.object + yield (obj, obj.matrix_world.copy()) + + def obj_ray_cast(obj, matrix): + """Wrapper for ray casting that moves the ray into object space""" + + # get the ray relative to the object + matrix_inv = matrix.inverted() + ray_origin_obj = matrix_inv @ ray_origin + ray_target_obj = matrix_inv @ ray_target + ray_direction_obj = ray_target_obj - ray_origin_obj + + # cast the ray + success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj) + + if success: + return location, normal, face_index + else: + return None, None, None + + # cast rays and find the closest object + best_length_squared = -1.0 + best_obj = None + face_normal = None + hit_loc = None + obj_face_index = None + for obj, matrix in visible_objects_and_duplis(): + if obj.type == 'MESH': + hit, normal, face_index = obj_ray_cast(obj, matrix) + if hit is not None: + hit_world = matrix @ hit + # scene.cursor.location = hit_world + length_squared = (hit_world - ray_origin).length_squared + if best_obj is None or length_squared < best_length_squared: + # print('length_squared',length_squared) + best_length_squared = length_squared + best_obj = obj + face_normal = normal + hit_loc = hit_world + obj_face_index = face_index + + if best_obj is not None: + best_original = best_obj.original + return best_original, hit_loc, face_normal, obj_face_index + + return None, None, None, None + + +# ----------------- +### Drawing +# ----------------- + +def circle_2d(coord, r, num_segments): + '''create circle, ref: http://slabode.exofire.net/circle_draw.shtml''' + cx, cy = coord + points = [] + theta = 2 * 3.1415926 / num_segments + c = math.cos(theta) #precalculate the sine and cosine + s = math.sin(theta) + x = r # we start at angle = 0 + y = 0 + for i in range(num_segments): + #bgl.glVertex2f(x + cx, y + cy) # output vertex + points.append((x + cx, y + cy)) + # apply the rotation matrix + t = x + x = c * x - s * y + y = s * t + c * y + + return points + +def draw_callback_px(self, context): + if context.area != self.current_area: + return + + # 50% alpha, 2 pixel width line + shader = gpu.shader.from_builtin('UNIFORM_COLOR') + gpu.state.blend_set('ALPHA') + gpu.state.line_width_set(2.0) + + view_rot = context.region_data.view_rotation + + if self.current_points: + for coord in self.current_points: + # circle3d = [(view_normal.to_track_quat('-Z', 'Z') @ cp) + coord for cp in self.circle_pts] + circle3d = [(view_rot @ cp) + coord for cp in self.mini_circle_pts] + circle3d.append(circle3d[0]) # Loop with last point + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": circle3d}) + shader.bind() + shader.uniform_float("color", (0.8, 0.8, 0.0, 0.9)) + batch.draw(shader) + + # Draw clicked path + if not self.point_list: + return + + positions = [pt['co'] for pt in self.point_list] + + line_color = color = (0.9, 0.1, 0.2, 0.8) + + ## Draw lines + + if len(self.point_list) >= 3: + ## Duplicate first position at last to loop triangle + positions += [self.point_list[0]['co']] + line_color = (0.9, 0.1, 0.3, 1.0) + + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": positions}) + shader.bind() + shader.uniform_float("color", line_color) + batch.draw(shader) + + # Draw circle on point aligned with view + + # view_normal = context.region_data.view_matrix.inverted() @ Vector((0,0,1)) + + for pt in self.point_list: + coord = pt['co'] + + # circle3d = [(view_normal.to_track_quat('-Z', 'Z') @ cp) + coord for cp in self.circle_pts] + circle3d = [(view_rot @ cp) + coord for cp in self.circle_pts] + circle3d.append(circle3d[0]) # Loop with last point + batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": circle3d}) + shader.bind() + shader.uniform_float("color", color) + batch.draw(shader) + + # restore opengl defaults + gpu.state.line_width_set(1.0) + gpu.state.blend_set('NONE') + + ## POST_PIXEL for text + + +# ----------------- +### Operator +# ----------------- + +class GP_OT_bind_points(bpy.types.Operator): + bl_idname = "gp.bind_points" + bl_label = "Bind Points" + bl_description = 'Bind points to use as reference for interpolation' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' + + clear : bpy.props.BoolProperty(name='Clear', default=False, options={'SKIP_SAVE'}) + + def invoke(self, context, event): + # print('INVOKE') + self.debug = False + self.gp = context.object + self.settings = context.scene.gp_interpo_settings + self.point_list = [] + self.current_points = [] + wm = context.window_manager + if tri_dump := wm.get(f'tri_{self.gp.name}'): + if self.clear: + del wm[f'tri_{self.gp.name}'] + return {'FINISHED'} + + ## load from current frame + ## Dict to list -> cast to dict (get ID prop array) + self.point_list = [dict(tri_dump[str(i)]) for i in range(3)] + + ## Update world coordinate position at current frame + dg = bpy.context.evaluated_depsgraph_get() + for point in self.point_list: + ob = bpy.context.scene.objects.get(point['object']) + ob_eval = ob.evaluated_get(dg) + point['co'] = ob_eval.matrix_world @ ob_eval.data.vertices[point['index']].co + + if self.clear: + return {'FINISHED'} + + ## Prepare circle 3D coordinate (create in invoke) + self.circle_pts = [Vector((p[0], p[1], 0)) for p in circle_2d((0,0), 0.01, 12)] + self.mini_circle_pts = [Vector((p[0], p[1], 0)) for p in circle_2d((0,0), 0.005, 12)] + + # self._timer = wm.event_timer_add(0.01, window=context.window) + + # draw in view space with 'POST_VIEW' and 'PRE_VIEW' + self.current_area = context.area + args = (self, context) + self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_VIEW') + context.area.header_text_set('Bind points | Enter: Valid | Backspace: remove last point | Esc / Right-Click: Cancel') + wm.modal_handler_add(self) + return {'RUNNING_MODAL'} + + def exit_modal(self, context, status='INFO', text=None): + # print('Exit modal') # Dbg + ## Reset all drawing and report + bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') + context.area.header_text_set(None) + context.area.tag_redraw() + if text: + self.report({status}, text) + else: + ## report standard info + self.report({'INFO'}, 'Done') + + def get_closest_vert(self, object_hit, hit_location, _normal, face_index, dg): + + ob_eval = object_hit.evaluated_get(dg) + ## Get closest index on face and store + face = ob_eval.data.polygons[face_index] + + ## list(dict) + vertices_infos = [{ + 'object': object_hit.name, # store object 'name' + 'index': vert_idx, # store vertex index + 'co': ob_eval.matrix_world @ ob_eval.data.vertices[vert_idx].co # store initial absolute coordinate + } + # vertex: ob_eval.data.vertices[vert_idx], + for vert_idx in face.vertices] + + ## Filter vertices by closest to hit_location + vertices_infos.sort(key=lambda x: (x['co'] - hit_location).length) + return vertices_infos[0] + + def bind(self, context): + ## store points on scene/wm properties associated with GP object + context.window_manager[f'tri_{context.object.name}'] = {str(i) : d for i, d in enumerate(self.point_list)} + self.exit_modal(context, text='Bound!') + + def modal(self, context, event): + context.area.tag_redraw() + if event.type in ('RIGHTMOUSE', 'ESC'): + context.area.header_text_set(f'Cancelling') + self.exit_modal(context, text='Cancelled') + return {'CANCELLED'} + + if event.type in ('WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'MIDDLEMOUSE'): + return {'PASS_THROUGH'} + + ## disable hint if too intensive + if event.type in {'MOUSEMOVE'}: + ## permanent update on closest point position (too heavy to always compute ?) + dg = bpy.context.evaluated_depsgraph_get() + object_hit, hit_location, _normal, face_index = raycast_objects(context, event, bpy.context.evaluated_depsgraph_get()) + if object_hit is None: + self.current_points = [] + else: + pt = self.get_closest_vert(object_hit, hit_location, _normal, face_index, dg) + self.current_points = [pt['co']] + return {'PASS_THROUGH'} + + elif event.type in ('BACK_SPACE', 'DEL') and event.value == 'PRESS': + if self.point_list: + self.report({'INFO'}, 'Removed last point') + self.point_list.pop() + + + elif event.type in ('RET', 'SPACE') and event.value == 'PRESS': + ## Valid + if len(self.point_list) < 3: + self.exit_modal(context, status='ERROR', text='Not enough point selected, Cancelling') + return {'CANCELLED'} + else: + self.bind(context) + return {'FINISHED'} + + elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': + if len(self.point_list) >= 3: + # self.report({'WARNING'}, 'Already got 3 point') + self.bind(context) + return {'FINISHED'} + else: + ## Raycast surface and store point + dg = bpy.context.evaluated_depsgraph_get() + + ## Basic rayvast (Do not consider object modifier or instance !) + # mouse = event.mouse_region_x, event.mouse_region_y + # view_mat = context.region_data.view_matrix.inverted() + # origin = view_mat.to_translation() + # depth3d = view_mat @ Vector((0, 0, -1)) + # point = utils.region_to_location(mouse, depth3d) + # ray = (point - origin) + # hit, hit_location, normal, face_index, object_hit, matrix = bpy.context.scene.ray_cast(dg, origin, ray) + + object_hit, hit_location, _normal, face_index = raycast_objects(context, event, dg) + + ## Also use triangle coordinate in triangle ?! using raycast with tesselated triangle infos + # object_hit, hit_location, tri, tri_indices = ray_cast_point(point, origin, dg) + + if object_hit is None: + self.report({'WARNING'}, 'Nothing hit, Retry on a surface') + else: + # print('object_hit: ', object_hit, object_hit.is_evaluated) + # print('hit_location: ', hit_location) + # print('face_index: ', face_index) + + # context.scene.cursor.location = hit_location # Dbg + + ### // get vert on-place + # ob_eval = object_hit.evaluated_get(dg) + # print('ob_eval: ', ob_eval) + # ## Get closest index on face and store + # face = ob_eval.data.polygons[face_index] + # ## Store list of tuples [(index, world_co, object_hit), ...] + # vertices_infos = [(vert_idx, + # ob_eval.matrix_world @ ob_eval.data.vertices[vert_idx].co, + # object_hit) # Store original object. + # for vert_idx in face.vertices] + + # ## Filter vedrtices by closest to hit_location + # vertices_infos.sort(key=lambda x: (x['co'] - hit_location).length) + + # vert = vertices_infos[0] + ### get vert on-place // + + vert = self.get_closest_vert(object_hit, hit_location, _normal, face_index, dg) + + # print('vert: ', vert) + + # if self.point_list and [x for x in self.point_list if vert[0] == x[0] and vert[3] == x[3]]: + if any(vert['index'] == x['index'] and vert['object'] == x['object'] for x in self.point_list): + self.report({'WARNING'}, "Cannot use same point twice !") + else: + self.point_list += [vert] + self.report({'INFO'}, f"Set point {len(self.point_list)}") + + return {'RUNNING_MODAL'} + + def execute(self, context): + return {"FINISHED"} + +classes = ( + GP_OT_bind_points, +) + +def register(): + for c in classes: + bpy.utils.register_class(c) + + +def unregister(): + for c in reversed(classes): + bpy.utils.unregister_class(c) diff --git a/interpolate_strokes/debug.py b/interpolate_strokes/debug.py index 2388576..11feb37 100644 --- a/interpolate_strokes/debug.py +++ b/interpolate_strokes/debug.py @@ -174,13 +174,28 @@ class GP_OT_debug_geometry_interpolation_targets(bpy.types.Operator): object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg) is_hit = 'HIT' if object_hit else '' - empty_at(name=f'{is_hit}search_{iteration}({ct})', pos=square_co, collection=debug_col, size=0.001, show_name=True) # type='SPHERE' + ## Show all search square elements + # empty_at(name=f'{is_hit}{i}search_{iteration}({ct})', pos=square_co, collection=debug_col, size=0.001, show_name=False) # type='SPHERE' + + ## Show only hit location (at hit location) + if is_hit: + empty_at(name=f'{is_hit}{i}search_{iteration}({ct})', pos=hit_location, collection=debug_col, size=0.001, show_name=False) # type='SPHERE' if object_hit: # and object_hit in col.all_objects[:]: context.scene.cursor.location = hit_location - ## Get location coplanar with triangle - hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri)) - empty_at(name=f'{object_hit.name}_{i}-{iteration}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=True) + ## On location coplanar with face triangle + # hit_location = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri)) + + ## On view plane + view_vec = context.scene.camera.matrix_world.to_quaternion() @ Vector((0,0,1)) + hit_location = intersect_line_plane(origin, point_co_world, hit_location, view_vec) + + ## An average of the two ? + # hit_location_1 = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri)) + # hit_location_2 = intersect_line_plane(origin, point_co_world, hit_location, view_vec) + # hit_location = (hit_location_1 + hit_location_2) / 2 + + empty_at(name=f'{object_hit.name}_{i}-{iteration}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=False) found = True # print(f'{si}:{i} iteration {iteration}') # Dbg break @@ -196,7 +211,7 @@ class GP_OT_debug_geometry_interpolation_targets(bpy.types.Operator): else: ## Hit at original position print(object_hit.name) - empty_at(name=f'{object_hit.name}_{i}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=True) + empty_at(name=f'{object_hit.name}_{i}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=False) print('Done') return {'FINISHED'} diff --git a/interpolate_strokes/operators.py b/interpolate_strokes/operators.py index 30fad0c..5beb21b 100644 --- a/interpolate_strokes/operators.py +++ b/interpolate_strokes/operators.py @@ -75,6 +75,12 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): self.frames_to_jump = None self.cancelled = False + ## Remove interpolation_plane collection ! (unseen, but can be hit) + if interp_plane := bpy.data.objects.get('interpolation_plane'): + bpy.data.objects.remove(interp_plane) + if interp_col := bpy.data.collections.get('interpolation_tool'): + bpy.data.collections.remove(interp_col) + context.window_manager.modal_handler_add(self) self._timer = context.window_manager.event_timer_add(0.01, window=context.window) context.area.header_text_set('Starting interpolation | Esc: Cancel') @@ -127,14 +133,16 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): # (frame: {self.frames_to_jump[self.loop_count]}) if self.status == 'START': + if self.settings.method == 'TRI' and not context.window_manager.get(f'tri_{self.gp.name}'): + self.exit_modal(context, status='ERROR', text='Need to bind coordinate first. Use "Bind Tri Point" button') + return {'CANCELLED'} + scn = bpy.context.scene ## Determine on what key/keys to jump self.frames_to_jump = following_keys(forward=self.next, all_keys=self.settings.use_animation) if not len(self.frames_to_jump): self.exit_modal(context, status='WARNING', text='No keyframe available in this direction') return {'CANCELLED'} - # print('self.frames_to_jump: ', self.frames_to_jump) - self.gp = context.object # matrix = np.array(self.gp.matrix_world, dtype='float64') @@ -154,6 +162,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): self.exit_modal(context, status='ERROR', text='No active frame') return {'CANCELLED'} + ## Get strokes to interpolate tgt_strokes = [s for s in self.gp.data.layers.active.active_frame.strokes if s.select] ## If nothing selected in sculpt/paint, Select all before triggering @@ -169,6 +178,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): included_cols = [c.name for c in self.gp.users_collection] target_obj = None + ## Setup depending on method if self.settings.method == 'BONE': if not self.settings.target_rig or not self.settings.target_bone: self.exit_modal(context, status='ERROR', text='No Bone selected') @@ -197,12 +207,6 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): target_obj = self.plane elif self.settings.method == 'GEOMETRY': - ## Remove interpolation_plane collection ! (unseen, but can be hit) - if interp_plane := bpy.data.objects.get('interpolation_plane'): - bpy.data.objects.remove(interp_plane) - if interp_col := bpy.data.collections.get('interpolation_tool'): - bpy.data.collections.remove(interp_col) - if col != context.scene.collection: included_cols.append(col.name) ## Maybe include a plane just behind geo ? probably bad idea @@ -257,6 +261,20 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): # Override collection col = intercol + if self.settings.method == 'TRI': + point_dict = context.window_manager.get(f'tri_{self.gp.name}') + ## point_dict -> {'0': {'object': object_name_as_str, 'index': 450}, ...} + + ## Get triangle dumped in context.window_manager + self.object_hit_source = object_hit = [bpy.context.scene.objects.get(point_dict[str(i)]['object']) for i in range(3)] + self.tri_indices_source = tri_indices = [point_dict[str(i)]['index'] for i in range(3)] # List of vertices index corresponding to tri coordinates + + tri = [] + dg = bpy.context.evaluated_depsgraph_get() + for source_obj, idx in zip(object_hit, tri_indices): + ob_eval = source_obj.evaluated_get(dg) + tri.append(ob_eval.matrix_world @ ob_eval.data.vertices[idx].co) + dg = bpy.context.evaluated_depsgraph_get() self.strokes_data = [] @@ -271,6 +289,17 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): stroke_data = [] for i, point in enumerate(stroke.points): point_co_world = world_co_3d[i] + + if self.settings.method == 'TRI': + ## Set hit location at same coordinate as point + # hit_location = point_co_world + ## Set hit location on tri plane + hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri)) + + # In this case object_hit is a list of object ! + stroke_data.append((stroke, point_co_world, object_hit, hit_location, tri, tri_indices)) + continue + if target_obj: object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(point_co_world), origin, dg) else: @@ -290,8 +319,18 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg) if object_hit: - ## Get location coplanar with triangle - hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri)) + ## On location coplanar with face triangle + # hit_location = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri)) + + ## On view plane + view_vec = context.scene.camera.matrix_world.to_quaternion() @ Vector((0,0,1)) + hit_location = intersect_line_plane(origin, point_co_world, hit_location, view_vec) + + ## An average of the two ? + # hit_location_1 = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri)) + # hit_location_2 = intersect_line_plane(origin, point_co_world, hit_location, view_vec) + # hit_location = (hit_location_1 + hit_location_2) / 2 + found = True # print(f'{si}:{i} iteration {iteration}') # Dbg break @@ -318,6 +357,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): print(f'Scan time {self.scan_time:.4f}s') # Copy stroke selection + bpy.ops.gpencil.select_linked() # Ensure whole stroke are selected before copy bpy.ops.gpencil.copy() # Jump frame and paste @@ -343,18 +383,26 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): arm=self.settings.target_rig, set_rotation=self.settings.use_bone_rotation, mesh=True) - + dg = bpy.context.evaluated_depsgraph_get() matrix_inv = np.array(self.gp.matrix_world.inverted(), dtype='float64')#.inverted() new_strokes = self.gp.data.layers.active.active_frame.strokes[-len(self.strokes_data):] + if self.settings.method == 'TRI': + ## All points have the same new frame triangle (tri_b) + tri_b = [] + for source_obj, idx in zip(self.object_hit_source, self.tri_indices_source): + ob_eval = source_obj.evaluated_get(dg) + tri_b.append(ob_eval.matrix_world @ ob_eval.data.vertices[idx].co) + # for new_stroke, stroke_data in zip(new_strokes, self.strokes_data): for new_stroke, stroke_data in zip(reversed(new_strokes), reversed(self.strokes_data)): world_co_3d = [] for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data: - eval_ob = object_hit.evaluated_get(dg) - tri_b = [eval_ob.data.vertices[i].co for i in tri_indices] - tri_b = matrix_transform(tri_b, eval_ob.matrix_world) + if not self.settings.method == 'TRI': + eval_ob = object_hit.evaluated_get(dg) + tri_b = [eval_ob.data.vertices[i].co for i in tri_indices] + tri_b = matrix_transform(tri_b, eval_ob.matrix_world) new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) # try: diff --git a/interpolate_strokes/properties.py b/interpolate_strokes/properties.py index 2ac2356..2b313a6 100644 --- a/interpolate_strokes/properties.py +++ b/interpolate_strokes/properties.py @@ -18,8 +18,9 @@ class GP_PG_interpolate_settings(PropertyGroup): name='Method', items= ( ('GEOMETRY', 'Geometry', 'Directly follow underlying geometry', 0), - ('BONE', 'Bone', 'Pick an armature bone and follow it', 1), - ('OBJECT', 'Object', 'Directly follow a specific object, even if occluded', 2), + ('OBJECT', 'Object Geometry', 'Same as Geometry mode, but target only a specific object, even if occluded (ignore all the others)', 1), + ('BONE', 'Bone', 'Pick an armature bone and follow it', 2), + ('TRI', 'Triangle', 'Interpolate based on triangle traced manually over geometry', 3), ), default='GEOMETRY', description='Select method for interpolating strokes' diff --git a/ui.py b/ui.py index bef6664..095c846 100755 --- a/ui.py +++ b/ui.py @@ -53,6 +53,7 @@ class GP_PT_interpolate(bpy.types.Panel): col.prop(settings, 'remove_occluded') elif settings.method == 'OBJECT': + col.prop(settings, 'search_range') col.prop(settings, 'target_object', text='Object') col.separator() @@ -76,6 +77,17 @@ class GP_PT_interpolate(bpy.types.Panel): row = col.row(align=True) row.operator('gp.debug_geometry_interpolation_targets', text='Debug points') row.operator('gp.debug_geometry_interpolation_targets', text='', icon='X').clear = True + + if context.scene.gp_interpo_settings.method == 'TRI': + col.separator() + row=layout.row(align=True) + wm = bpy.context.window_manager + binded = context.object and context.object.type == 'GPENCIL' and wm.get(f'tri_{context.object.name}') + txt = 'Show Tri Points' if binded else 'Bind Tri Points' + row.operator('gp.bind_points', text=txt, icon='MESH_DATA') + if binded: + row.operator('gp.bind_points', text='', icon='TRASH').clear = True + classes = ( GP_PT_interpolate, diff --git a/utils.py b/utils.py index 7bab0c0..7c5d9f4 100644 --- a/utils.py +++ b/utils.py @@ -124,7 +124,7 @@ def ray_cast_point(point, origin, depsgraph): tri, tri_indices = get_tri_from_face(hit_location, face_index, object_hit, depsgraph) - return object_hit, np.array(hit_location), tri, tri_indices + return object_hit, hit_location, tri, tri_indices def obj_ray_cast(obj, point, origin, depsgraph): """Wrapper for ray casting that moves the ray into object space""" @@ -142,7 +142,7 @@ def obj_ray_cast(obj, point, origin, depsgraph): # Get hit location world_space hit_location = obj.matrix_world @ location tri, tri_indices = get_tri_from_face(hit_location, face_index, obj, depsgraph) - return obj, np.array(hit_location), tri, tri_indices + return obj, hit_location, tri, tri_indices def empty_at(name='Empty', pos=(0,0,0), collection=None, type='PLAIN_AXES', size=1, show_name=False):