From 1daee04d9c806159f4f5b0ea83974ba7d05cc57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Perdig=C3=A3o?= Date: Mon, 1 Jun 2026 22:18:13 -0300 Subject: [PATCH] Early-terminate solid raycasts in non-xray mode --- src/bonsai/bonsai/tool/raycast.py | 133 ++++++++++++++++++++++-------- 1 file changed, 100 insertions(+), 33 deletions(-) diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index 327ebe6322..f5440f9a80 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -800,6 +800,30 @@ class Raycast(bonsai.core.tool.Raycast): else: return None, None, None + @classmethod + def process_wireframe_snap_obj( + cls, + context: bpy.types.Context, + event: bpy.types.Event, + snap_obj, + ray_origin: Vector, + closest_snaps: list, + ): + snap_points = tool.Raycast.ray_cast_by_proximity_2d(context, event, snap_obj) + hit_obj = None + hit = None + if snap_points: + closest_length_squared = float("inf") + for point in snap_points: + point["group"] = "Wireframe" + closest_snaps.append(point) + length = (point["point"] - ray_origin).length_squared + if length < closest_length_squared: + closest_length_squared = length + hit = point["point"] + hit_obj = point["object"] + return hit_obj, hit + @classmethod def ray_cast_and_get_closest_to_camera_snaps( cls, @@ -814,35 +838,45 @@ class Raycast(bonsai.core.tool.Raycast): ray_origin, ray_target, ray_direction = cls.get_viewport_ray_data(context, event) + space = context.space_data + xray_mode = (space.shading.type == "SOLID" and space.shading.show_xray) or ( + space.shading.type == "WIREFRAME" and space.shading.show_xray_wireframe + ) + closest_snaps = [] - hit = None - for snap_obj in objs_to_raycast: - if snap_obj.obj.type in {"EMPTY", "CURVE"} or ( - hasattr(snap_obj.obj.data, "polygons") and len(snap_obj.obj.data.polygons) == 0 - ): - # For wireframe objects we have to test all the snaps to see which is closer - snap_points = tool.Raycast.ray_cast_by_proximity_2d(context, event, snap_obj) - closest_wf_hit = None - closest_wf_length_squared = 1.0 - closest_wf_point = None - if snap_points: - for point in snap_points: - point["group"] = "Wireframe" - closest_snaps.append(point) - length = (point["point"] - ray_origin).length_squared - if closest_wf_hit is None or length < closest_wf_length_squared: - closest_wf_length_squared = length - closest_wf_hit = point["point"] - closest_wf_point = point + if not xray_mode and objs_to_raycast: + # Non-xray - only the closest solid object's Face snap is kept by + # the caller (detect_snapping_points). Process solids in distance + # order and stop at the first hit to minimise raycasts. + wireframe_objs = [] + solid_objs = [] + for snap_obj in objs_to_raycast: + if snap_obj.obj.type in {"EMPTY", "CURVE"} or ( + hasattr(snap_obj.obj.data, "polygons") and len(snap_obj.obj.data.polygons) == 0 + ): + wireframe_objs.append(snap_obj) + else: + solid_objs.append(snap_obj) - if closest_wf_point: - hit_obj = closest_wf_point["object"] - hit = closest_wf_point["point"] - face_index = None + # Rough distance - object origin to ray origin + solid_objs.sort(key=lambda so: (so.obj.matrix_world.translation - ray_origin).length_squared) - else: - # Solid objects + # Process wireframe objects first (all of them, always collected) + for snap_obj in wireframe_objs: + hit_obj, hit = cls.process_wireframe_snap_obj( + context, event, snap_obj, ray_origin, closest_snaps + ) + if hit is not None: + length_squared = (hit - ray_origin).length_squared + if closest_obj is None or length_squared < closest_length_squared: + closest_length_squared = length_squared + closest_obj = hit_obj + closest_hit = hit + closest_face_index = None + + # Process solid objects in distance order, stop at first hit + for snap_obj in solid_objs: hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, snap_obj.obj) if hit: @@ -856,14 +890,47 @@ class Raycast(bonsai.core.tool.Raycast): } closest_snaps.append(snap_point) - # Here we test which is closer, including wireframe and solid objects - if hit is not None: - length_squared = (hit - ray_origin).length_squared - if closest_obj is None or length_squared < closest_length_squared: - closest_length_squared = length_squared - closest_obj = hit_obj - closest_hit = hit - closest_face_index = face_index + length_squared = (hit - ray_origin).length_squared + if closest_obj is None or length_squared < closest_length_squared: + closest_length_squared = length_squared + closest_obj = hit_obj + closest_hit = hit + closest_face_index = face_index + + break + + else: + # Xray mode - process all objects (all snaps are kept by the caller) + for snap_obj in objs_to_raycast: + if snap_obj.obj.type in {"EMPTY", "CURVE"} or ( + hasattr(snap_obj.obj.data, "polygons") and len(snap_obj.obj.data.polygons) == 0 + ): + hit_obj, hit = cls.process_wireframe_snap_obj( + context, event, snap_obj, ray_origin, closest_snaps + ) + face_index = None + else: + # Solid objects + hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, snap_obj.obj) + + if hit: + snap_point = { + "point": hit, + "type": "Face", + "group": "Object", + "object": hit_obj, + "face_index": face_index, + "distance": 9, # High value so it has low priority + } + closest_snaps.append(snap_point) + + if hit is not None: + length_squared = (hit - ray_origin).length_squared + if closest_obj is None or length_squared < closest_length_squared: + closest_length_squared = length_squared + closest_obj = hit_obj + closest_hit = hit + closest_face_index = face_index # Label snaps from the closest object if closest_obj is not None: