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):