From 6bbf3d39177fafc0dda4ccbe42f91b3fced7663a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Sun, 25 Aug 2024 18:23:22 -0300 Subject: [PATCH] Refactor snapping functions and allow to choose snapping point. By pressing "M", the user can now cycle through active snapping points when there are many options close to each other. Still lacks a text to give information about the active point. --- src/bonsai/bonsai/bim/module/model/wall.py | 13 +- .../bonsai/bim/module/project/operator.py | 13 +- src/bonsai/bonsai/tool/raycast.py | 39 ++++- src/bonsai/bonsai/tool/snap.py | 151 +++++++++++------- 4 files changed, 148 insertions(+), 68 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index 7af2637404..78f2c25725 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -324,6 +324,7 @@ class DrawPolylineWall(bpy.types.Operator): self.input_value_xy = [None, None] self.input_panel = {"D": "", "A": "", "X": "", "Y": ""} self.snap_angle = None + self.snapping_points = [] def recalculate_inputs(self, context): if self.number_input: @@ -378,7 +379,7 @@ class DrawPolylineWall(bpy.types.Operator): self.is_input_on = False self.input_type = "OFF" PolylineDecorator.set_input_panel(self.input_panel, self.input_type) - tool.Snap.clear_snaping_ref() + tool.Snap.clear_snapping_ref() tool.Blender.update_viewport() else: self.mousemove_count = 0 @@ -389,7 +390,8 @@ class DrawPolylineWall(bpy.types.Operator): self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj)) if self.mousemove_count > 3: - tool.Snap.snaping_movement(context, event, self.objs_2d_bbox) + detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) + self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) tool.Blender.update_viewport() @@ -480,6 +482,10 @@ class DrawPolylineWall(bpy.types.Operator): PolylineDecorator.set_input_panel(self.input_panel, self.input_type) tool.Blender.update_viewport() + if event.value == "PRESS" and event.type == "M": + self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) + tool.Blender.update_viewport() + if event.type in {"MIDDLEMOUSE", "WHEELUPMOUSE", "WHEELDOWNMOUSE"}: return {"PASS_THROUGH"} @@ -508,7 +514,8 @@ class DrawPolylineWall(bpy.types.Operator): self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj)) - tool.Snap.snaping_movement(context, event, self.objs_2d_bbox) + detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) + self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index 9bf6fb0376..f04c4c98db 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2333,6 +2333,7 @@ class MeasureTool(bpy.types.Operator): self.input_value_xy = [None, None] self.input_panel = {"D": "", "A": "", "X": "", "Y": "", "Z": ""} self.snap_angle = None + self.snapping_points = [] def recalculate_inputs(self, context): if self.number_input: @@ -2361,7 +2362,7 @@ class MeasureTool(bpy.types.Operator): self.is_input_on = False self.input_type = "OFF" PolylineDecorator.set_input_panel(self.input_panel, self.input_type) - tool.Snap.clear_snaping_ref() + tool.Snap.clear_snapping_ref() tool.Blender.update_viewport() else: self.mousemove_count = 0 @@ -2372,7 +2373,8 @@ class MeasureTool(bpy.types.Operator): self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj)) if self.mousemove_count > 3: - tool.Snap.snaping_movement(context, event, self.objs_2d_bbox) + detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) + self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) PolylineDecorator.set_input_panel(self.input_panel, self.input_type) @@ -2456,6 +2458,10 @@ class MeasureTool(bpy.types.Operator): PolylineDecorator.set_input_panel(self.input_panel, self.input_type) tool.Blender.update_viewport() + if event.value == "PRESS" and event.type == "M": + self.snapping_points = tool.Snap.modify_snapping_point_selection(self.snapping_points) + tool.Blender.update_viewport() + if event.shift and event.value == "PRESS" and event.type == "X": tool.Snap.set_use_default_container(False) PolylineDecorator.set_use_default_container(False) @@ -2499,7 +2505,8 @@ class MeasureTool(bpy.types.Operator): self.visible_objs = tool.Raycast.get_visible_objects(context) for obj in self.visible_objs: self.objs_2d_bbox.append(tool.Raycast.get_objects_2d_bounding_boxes(context, obj)) - tool.Snap.snaping_movement(context, event, self.objs_2d_bbox) + detected_snaps = tool.Snap.detect_snapping_points(context, event, self.objs_2d_bbox) + self.snapping_points = tool.Snap.select_snapping_points(context, event, detected_snaps) PolylineDecorator.set_mouse_position(event) self.input_panel = PolylineDecorator.calculate_distance_and_angle(context, self.is_input_on) tool.Blender.update_viewport() diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index c205122745..148b11496d 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -106,12 +106,14 @@ class Raycast(bonsai.core.tool.Raycast): else: return None, None, None + @classmethod def ray_cast_by_proximity(cls, context, event, obj, mesh=None): region = context.region rv3d = context.region_data mouse_pos = event.mouse_region_x, event.mouse_region_y ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event) + points = [] try: loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction) @@ -119,9 +121,9 @@ class Raycast(bonsai.core.tool.Raycast): loc = Vector((0, 0, 0)) bm = bmesh.new() - if mesh is None: + if mesh is None: # Object with faces bm.from_mesh(obj.data) - else: + else: # Object without faces verts = [bm.verts.new(obj.data.vertices[i].co) for i in mesh.vertices] bm.faces.new(verts) @@ -134,14 +136,14 @@ class Raycast(bonsai.core.tool.Raycast): intersection, _ = mathutils.geometry.intersect_point_line(division_point, ray_target, loc) distance = (division_point - intersection).length if distance < 0.2: - return division_point, "Edge Center" + points.append((division_point, "Edge Center")) for vertex in bm.verts: world_vertex = obj.matrix_world @ vertex.co intersection, _ = mathutils.geometry.intersect_point_line(world_vertex, ray_target, loc) distance = (world_vertex - intersection).length if distance < 0.2: - return world_vertex, "Vertex" + points.append((world_vertex, "Vertex")) for edge in bm.edges: v1 = edge.verts[0].co @@ -151,11 +153,36 @@ class Raycast(bonsai.core.tool.Raycast): intersection = mathutils.geometry.intersect_line_line(ray_target, loc, world_v1, world_v2) distance = (intersection[0] - intersection[1]).length if distance < 0.2: - return intersection[1], "Edge" + points.append((intersection[1], "Edge")) - return None, None + return points + @classmethod + def ray_cast_to_polyline(cls, context, event): + region = context.region + rv3d = context.region_data + mouse_pos = event.mouse_region_x, event.mouse_region_y + ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event) + + try: + loc = view3d_utils.region_2d_to_location_3d(region, rv3d, mouse_pos, ray_direction) + except: + loc = Vector((0, 0, 0)) + + polyline_data = bpy.context.scene.BIMModelProperties.polyline_point + polyline_points = [] + for point_data in polyline_data: + point = Vector((point_data.x, point_data.y, point_data.z)) + + + intersection, _ = mathutils.geometry.intersect_point_line(point, ray_target, loc) + distance = (point - intersection).length + if distance < 0.2: + polyline_points.append((point, "Vertex")) + + return polyline_points + @classmethod def ray_cast_to_plane(cls, context, event, plane_origin, plane_normal): region = context.region diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 473faf3ca8..ffc123db96 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -113,7 +113,7 @@ class Snap(bonsai.core.tool.Snap): snap_vertex.snap_type = snap_type @classmethod - def clear_snaping_ref(cls): + def clear_snapping_ref(cls): bpy.context.scene.BIMModelProperties.snap_mouse_ref.clear() @classmethod @@ -294,10 +294,11 @@ class Snap(bonsai.core.tool.Snap): # cls.update_snaping_point(snap_point[0], snap_point[1]) @classmethod - def snaping_movement(cls, context, event, objs_2d_bbox): + def detect_snapping_points(cls, context, event, objs_2d_bbox): region = context.region rv3d = context.region_data cls.mouse_pos = event.mouse_region_x, event.mouse_region_y + detected_snaps = [] offset = 15 snap_threshold = 0.3 @@ -376,12 +377,38 @@ class Snap(bonsai.core.tool.Snap): else: return None, None, None + + ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event) + + objs_to_raycast = [] + for obj, bbox_2d in objs_2d_bbox: + if obj.type == "MESH" and bbox_2d: + if tool.Raycast.in_view_2d_bounding_box(cls.mouse_pos, bbox_2d): + objs_to_raycast.append(obj) + # Obj + snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast) + if hit is not None: + detected_snaps.append({"Object": (snap_obj, hit, face_index)}) + + # Edge-Vertex + for obj in objs_to_raycast: + options = tool.Raycast.ray_cast_by_proximity(context, event, obj) + snap_obj = obj + if options: + detected_snaps.append({"Edge-Vertex": (snap_obj, options)}) + break + # Polyline try: polyline_data = bpy.context.scene.BIMModelProperties.polyline_point last_polyline_point = polyline_data[len(polyline_data) - 1] except: last_polyline_point = None + snap_points = tool.Raycast.ray_cast_to_polyline(context, event) + # snap_point = cls.select_snap_point(snap_points, intersection, snap_threshold) + if snap_points: + detected_snaps.append({"Polyline": snap_points}) + # Axis and Plane elevation = tool.Ifc.get_object(tool.Root.get_default_container()).location.z plane_origin, plane_normal = select_plane_method() @@ -391,67 +418,79 @@ class Snap(bonsai.core.tool.Snap): axis_start = None axis_end = None if cls.snap_plane_method in {"XY", "XZ", "YZ"}: - # Locks snap into an angle axis - if event.shift: - rot_intersection, _, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle) - else: - cls.snap_angle = None - rot_intersection, cls.snap_angle, _, _ = cls.snap_on_axis(intersection) - else: - rot_intersection = None + rot_intersection, snap_angle, axis_start, axis_end = cls.snap_on_axis(intersection, cls.snap_angle) + if rot_intersection: + detected_snaps.append({"Axis": rot_intersection}) - ray_origin, ray_target, ray_direction = tool.Raycast.get_viewport_ray_data(context, event) + detected_snaps.append({"Plane": intersection}) - objs_to_raycast = [] - for obj, bbox_2d in objs_2d_bbox: - if obj.type == "MESH" and bbox_2d: - if tool.Raycast.in_view_2d_bounding_box(cls.mouse_pos, bbox_2d): - objs_to_raycast.append(obj) + for item in detected_snaps: - # Try to get object using object raycast method - # If it fails use the raycast by proximity - snap_obj, hit, face_index = cast_rays_and_get_best_object(objs_to_raycast) - if snap_obj is None: - for obj in objs_to_raycast: - hit, hit_type = tool.Raycast.ray_cast_by_proximity(context, event, obj) - snap_obj = obj - if hit is not None: - break + return detected_snaps - if snap_obj is not None: - # Objects with faces - if face_index is not None: - snap_point = cls.get_snap_points_on_raycasted_face(context, event, snap_obj, face_index) - if snap_point[0] is None: - cls.update_snaping_point(hit, "Face") - return - # Objects with only edges or verts - else: - if hit is not None: - snap_point = (hit, hit_type) + + @classmethod + def select_snapping_points(cls, context, event, detected_snaps): + snapping_points = [] + for origin in detected_snaps: + + if "Object" in list(origin.keys()): + snap_obj, hit, face_index = origin['Object'] + + matrix = snap_obj.matrix_world.copy() + face = snap_obj.data.polygons[face_index] + verts = [] + for i in face.vertices: + verts.append(matrix @ snap_obj.data.vertices[i].co) + + options = tool.Raycast.ray_cast_by_proximity(context, event, snap_obj, face) + if not options: + snapping_points.append((hit, "Face")) else: - snap_point = None + for op in options: + snapping_points.append(op) - # If there are no objects to snap to, it will try snaping in the polyline - else: - snap_points = cls.get_snap_points_on_polyline() - snap_point = cls.select_snap_point(snap_points, intersection, snap_threshold) + cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) - if snap_point: - if event.shift and axis_start: - snap_result, snap_type = cls.mix_snap_and_axis(snap_point, axis_start, axis_end, elevation) - cls.update_snaping_point(snap_result, snap_type) - cls.update_snaping_ref(snap_point[0], snap_point[1]) - return - else: - cls.update_snaping_point(snap_point[0], snap_point[1]) - return - # If theres no snapping point from objects and polyline - # It will snap to the plane - if rot_intersection: - cls.update_snaping_point(rot_intersection, "Axis") - else: - cls.update_snaping_point(intersection, "Plane") + return snapping_points + + elif "Edge-Vertex" in list(origin.keys()): + snap_obj, options = origin['Edge-Vertex'] + for op in options: + snapping_points.append(op) + + cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + return snapping_points + + elif "Polyline" in list(origin.keys()): + options = origin['Polyline'] + for op in options: + snapping_points.append(op) + + cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + return snapping_points + + elif "Axis" in list(origin.keys()): + intersection = origin['Axis'] + snapping_points.append((intersection, "Axis")) + + cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + return snapping_points + + elif "Plane" in list(origin.keys()): + intersection = origin['Plane'] + snapping_points.append((intersection, "Plane")) + + cls.update_snaping_point(snapping_points[0][0], snapping_points[0][1]) + return snapping_points + + + @classmethod + def modify_snapping_point_selection(cls, snapping_points): + shifted_list = snapping_points[1:] + snapping_points[:1] + cls.update_snaping_point(shifted_list[0][0], shifted_list[0][1]) + return shifted_list + @classmethod def validate_input(cls, input_number, input_type):