From 4e6f1d10a8c60a0d1fb4922a39ee0691bf665851 Mon Sep 17 00:00:00 2001 From: Albert Rodriguez Franco Date: Thu, 18 Mar 2021 00:10:33 +0100 Subject: [PATCH] Adding Geodesic Keypoint Erasing (#11) * Adding point deletion support * Bumping up version plus adding comments --- __init__.py | 2 +- addon/operator/geopath_datastructure.py | 155 +++++++++++++------ addon/operator/measures_geodesic_operator.py | 41 ++++- 3 files changed, 144 insertions(+), 54 deletions(-) diff --git a/__init__.py b/__init__.py index 7977061..5b69763 100644 --- a/__init__.py +++ b/__init__.py @@ -3,7 +3,7 @@ "author": "Albert Rodriguez", "description": "Tools to take measures for Avatar", "blender": (2, 92, 0), - "version": (0, 4, 1), + "version": (0, 4, 5), "location": "View3D > Toolshelf", "warning": "This plugin is only compatible with Blender 2.92", "category": "Add measures", diff --git a/addon/operator/geopath_datastructure.py b/addon/operator/geopath_datastructure.py index 00c48bf..a8e446e 100644 --- a/addon/operator/geopath_datastructure.py +++ b/addon/operator/geopath_datastructure.py @@ -38,6 +38,7 @@ def __init__(self, context, selected_obj): self.epsilon = .0000001 self.max_iters = 100000 + self.distance_threshold = 0.006 # geos, fixed, close, far self.geo_data = [None, None] @@ -111,53 +112,6 @@ def grab_mouse_move(self, context, x, y): # Finally move the key_point self.key_points[point_pos] = (hit_loc, hit_face) - def redo_geodesic_segment(self, segment_pos, start_loc, - start_face, end_loc, end_face, - cache_pos): - - # Special case handling for weird algorithm behavior - should_reverse = cache_pos != 1 - - # Try using the cached structure before relaunching - # a new geodesic walk - cached_path = self.try_continue_geodesic_walk( - cache_pos, end_loc, end_face) - - if cached_path: - self.cleanup_path(start_loc, end_loc, cached_path, should_reverse) - self.path_segments[segment_pos] = cached_path - return - - geos, fixed, close, far = geodesic_walk( - self.bme.verts, start_face, start_loc, - end_face, self.max_iters) - - path_elements, path = gradient_descent( - geos, end_face, end_loc, self.epsilon) - - self.geo_data[cache_pos] = (geos, fixed, close, far) - - self.cleanup_path(start_loc, end_loc, path, should_reverse) - self.path_segments[segment_pos] = path - - def try_continue_geodesic_walk(self, cache_pos, hit_loc, hit_face): - - # Data was not cached - if self.geo_data[cache_pos] is None: - return None - - geos, fixed, close, far = self.geo_data[cache_pos] - - if not all([v in fixed for v in hit_face.verts]): - continue_geodesic_walk( - geos, fixed, close, far, - hit_face, self.max_iters) - - path_elements, path = gradient_descent( - geos, hit_face, hit_loc, self.epsilon) - - return path - def grab_start(self): if (self.hover_point_index is None): @@ -232,6 +186,107 @@ def grab_finish(self): return + def erase_mouse_move(self, context, x, y): + + hit, hit_loc, face_ind = self.raycast(context, x, y) + + if not hit: + return + + # look for keypoints to hover + self.find_keypoint_hover(hit_loc) + + def erase_point(self): + + if (self.hover_point_index is None): + return + + point_pos = self.hover_point_index + + # Reset hovering point + self.hover_point_index = None + + # I have a segment before point + segment_before = None + if point_pos > 0: + segment_before = self.path_segments[point_pos-1] + + # I have a segment after point + segment_after = None + if point_pos < len(self.key_points)-1: + segment_after = self.path_segments[point_pos] + + if segment_before: + self.path_segments.remove(segment_before) + if segment_after: + self.path_segments.remove(segment_after) + + # Remove position from keypoints + self.key_points.pop(point_pos) + + # Redo geodesic path if needed + if segment_before and segment_after: + start_loc, start_face = self.key_points[point_pos-1] + end_loc, end_face = self.key_points[point_pos] + # Recreate the position + self.path_segments.insert(point_pos-1, []) + self.redo_geodesic_segment( + point_pos-1, start_loc, start_face, end_loc, end_face, 0) + + # Avoid having garbage in the geo cache + self.geo_data[0] = None + + def erase_cancel(self): + # Reset hovering point + self.hover_point_index = None + + def redo_geodesic_segment(self, segment_pos, start_loc, + start_face, end_loc, end_face, + cache_pos): + + # Special case handling for weird algorithm behavior + should_reverse = cache_pos != 1 + + # Try using the cached structure before relaunching + # a new geodesic walk + cached_path = self.try_continue_geodesic_walk( + cache_pos, end_loc, end_face) + + if cached_path: + self.cleanup_path(start_loc, end_loc, cached_path, should_reverse) + self.path_segments[segment_pos] = cached_path + return + + geos, fixed, close, far = geodesic_walk( + self.bme.verts, start_face, start_loc, + end_face, self.max_iters) + + path_elements, path = gradient_descent( + geos, end_face, end_loc, self.epsilon) + + self.geo_data[cache_pos] = (geos, fixed, close, far) + + self.cleanup_path(start_loc, end_loc, path, should_reverse) + self.path_segments[segment_pos] = path + + def try_continue_geodesic_walk(self, cache_pos, hit_loc, hit_face): + + # Data was not cached + if self.geo_data[cache_pos] is None: + return None + + geos, fixed, close, far = self.geo_data[cache_pos] + + if not all([v in fixed for v in hit_face.verts]): + continue_geodesic_walk( + geos, fixed, close, far, + hit_face, self.max_iters) + + path_elements, path = gradient_descent( + geos, hit_face, hit_loc, self.epsilon) + + return path + def draw(self, context, plugin_state): mx = self.selected_obj.matrix_world @@ -252,7 +307,7 @@ def draw(self, context, plugin_state): self.point_size, self.point_select_color) - if plugin_state == Geodesic_State.GRAB: + if plugin_state in {Geodesic_State.GRAB, Geodesic_State.ERASE}: draw.draw_3d_circles(context, points, self.circle_radius, self.point_color) if point_highlight_idx is not None: @@ -311,7 +366,8 @@ def find_keypoint_hover(self, point): key_points = [key_point for (key_point, key_face) in self.key_points] selected_keypoints = list( - filter(lambda x: (x-point).length <= 0.006, key_points) + filter(lambda x: (x-point).length <= self.distance_threshold, + key_points) ) if selected_keypoints: @@ -322,3 +378,4 @@ def find_keypoint_hover(self, point): class Geodesic_State(Enum): MAIN = 1 GRAB = 2 + ERASE = 3 diff --git a/addon/operator/measures_geodesic_operator.py b/addon/operator/measures_geodesic_operator.py index 5007a42..ca8b85a 100644 --- a/addon/operator/measures_geodesic_operator.py +++ b/addon/operator/measures_geodesic_operator.py @@ -49,6 +49,8 @@ def modal(self, context, event): return self.handle_main(context, event) elif self.state == Geodesic_State.GRAB: return self.handle_grab(context, event) + elif self.state == Geodesic_State.ERASE: + return self.handle_erase(context, event) return {"RUNNING_MODAL"} # Should not get here but you never know @@ -66,6 +68,10 @@ def handle_main(self, context, event): elif event.type == 'G' and event.value == 'PRESS': self.state = Geodesic_State.GRAB # Do grab mode + elif event.type == 'E' and event.value == 'PRESS': + context.window.cursor_set("ERASER") + self.state = Geodesic_State.ERASE # Do erase mode + # Adding points elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': x, y = (event.mouse_region_x, event.mouse_region_y) @@ -94,15 +100,16 @@ def handle_grab(self, context, event): }: return {'PASS_THROUGH'} - if event.type == 'MOUSEMOVE': + elif event.type == 'MOUSEMOVE': x, y = (event.mouse_region_x, event.mouse_region_y) self.geopath.grab_mouse_move(context, x, y) # try to see if we are grabbing - if event.type == 'LEFTMOUSE' and event.value == 'PRESS': + elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': self.geopath.grab_start() - if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + # stop grabbing when releasing + elif event.type == 'LEFTMOUSE' and event.value == 'RELEASE': self.geopath.grab_finish() # cancel grabbing @@ -115,6 +122,33 @@ def handle_grab(self, context, event): context.area.tag_redraw() return {'RUNNING_MODAL'} + def handle_erase(self, context, event): + + # Free navigation + if event.type in { + 'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', + 'WHEELINMOUSE', 'WHEELOUTMOUSE' + }: + return {'PASS_THROUGH'} + + elif event.type == 'MOUSEMOVE': + x, y = (event.mouse_region_x, event.mouse_region_y) + self.geopath.erase_mouse_move(context, x, y) + + elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': + self.geopath.erase_point() + + # cancel erasing + elif (event.type in {'RIGHTMOUSE', 'ESC', 'E'} + and event.value == 'PRESS'): + + self.geopath.erase_cancel() + context.window.cursor_set("DEFAULT") + self.state = Geodesic_State.MAIN + + context.area.tag_redraw() + return {'RUNNING_MODAL'} + def detect_collision(self, context, event): if event.type == 'MOUSEMOVE': self.hit_point = None @@ -142,7 +176,6 @@ def execute(self, context): for i in range(1, len(path)): edges.append(bm.edges.new((vertices[i-1], vertices[i]))) - # print("adding {:3f} - {:3f}".format(i-1, i)) me = bpy.data.meshes.new("GeodesicPath") bm.to_mesh(me)