snap: use GPU detection for solid and wireframe objects.

Now is also works with x-ray mode.
This commit is contained in:
Bruno Perdigão
2026-08-16 13:56:48 -03:00
committed by Bruno Perdigão
parent 9673f01c21
commit 209d487f5f
2 changed files with 77 additions and 52 deletions
+47 -32
View File
@@ -750,6 +750,11 @@ class Raycast(bonsai.core.tool.Raycast):
gpu.state.blend_set("NONE")
gpu.state.face_culling_set("NONE")
read_size = 2 * _SNAP_RADIUS_PX + 1
read_x = max(0, min(mx - _SNAP_RADIUS_PX, w - read_size))
read_y = max(0, min(my - _SNAP_RADIUS_PX, h - read_size))
buffers_list = []
with _offscreen.bind():
fb = gpu.state.active_framebuffer_get()
fb.clear(color=(0.0, 0.0, 0.0, 0.0), depth=1.0)
@@ -762,35 +767,44 @@ class Raycast(bonsai.core.tool.Raycast):
gpu.matrix.load_matrix(Matrix.Identity(4))
batch.draw(_encoding_shader)
read_size = 2 * _SNAP_RADIUS_PX + 1
read_x = max(0, min(mx - _SNAP_RADIUS_PX, w - read_size))
read_y = max(0, min(my - _SNAP_RADIUS_PX, h - read_size))
buf = fb.read_color(int(read_x), int(read_y),
buf = fb.read_color(int(read_x), int(read_y),
read_size, read_size, 4, 0, "UBYTE")
if xray_mode: # gets all buffers
buffers_list.append(buf)
last_buf = buf # gets only the closest buffer
# Restore state
gpu.state.depth_mask_set(True)
gpu.state.depth_test_set("LESS")
pixel_data = buf.to_list()
if not pixel_data or not pixel_data[0]:
return [], None
if tris:
# Decode hits
hits: set[tuple[int, int]] = set()
hits: set[int] = set()
if xray_mode:
for row in pixel_data:
for px in row:
vals_read: set[int] = set()
for buf in buffers_list:
pixel_data = buf.to_list()
if not pixel_data or not pixel_data[0]:
return [], None
centre_x = mx - int(read_x)
centre_y = my - int(read_y)
if 0 <= centre_y < len(pixel_data) and 0 <= centre_x < len(pixel_data[0]):
px = pixel_data[centre_y][centre_x]
val = _decode_wireframe_pixel(px[0], px[1], px[2], px[3])
if val in vals_read: # avoid getting all the tris from the same object
continue
vals_read.add(val)
if val > 0:
val -= 1
obj_index = int(val) >> _TRI_OBJ_SHIFT
face_index = int(val) & _TRI_FACE_MASK
if obj_index < len(_obj_list):
hits.add((obj_index, face_index))
hits.add(obj_index)
else:
pixel_data = last_buf.to_list()
if not pixel_data or not pixel_data[0]:
return [], None
centre_x = mx - int(read_x)
centre_y = my - int(read_y)
if 0 <= centre_y < len(pixel_data) and 0 <= centre_x < len(pixel_data[0]):
@@ -799,42 +813,43 @@ class Raycast(bonsai.core.tool.Raycast):
if val > 0:
val -= 1
obj_index = int(val) >> _TRI_OBJ_SHIFT
face_index = int(val) & _TRI_FACE_MASK
if obj_index < len(_obj_list):
hits.add((obj_index, face_index))
hits.add(obj_index)
if not hits:
return []
return [], None
snaps: list[dict] = []
closest_obj = None
closest_dist = float("inf")
ray_origin, _, _ = cls.get_viewport_ray_data(context, event)
objs_to_raycast = []
for obj_index, face_index in hits:
for obj_index in hits:
obj = _obj_list[obj_index]
objs_to_raycast.append(obj)
hit_obj, hit, face_index = cls.cast_rays_and_get_best_object(context, event, objs_to_raycast)
hit_obj, hit, face_index = cls.cast_rays_to_single_object(context, event, obj)
if hit:
snap: dict = {
"point": hit,
"type": "Face",
"group": "Object",
"object": hit_obj,
"face_index": face_index,
"distance": 9, # High value so it has low priority
}
dist = (hit - ray_origin).length
if dist < closest_dist:
closest_dist = dist
closest_obj = obj
snap: dict = {
"point": hit,
"type": "Face",
"group": "Object",
"object": obj,
"face_index": face_index,
"distance": 9, # High value so it has low priority
}
snaps.append(snap)
snaps.append(snap)
return snaps, closest_obj
else:
centre = (mx - int(read_x), my - int(read_y))
pixel_data = last_buf.to_list()
best = _find_closest_wireframe_pixel(pixel_data, *centre)
if best is None:
return []
return [], None
encoded, dx, dy = best
# Decode and build snap dicts
@@ -931,7 +946,7 @@ class Raycast(bonsai.core.tool.Raycast):
})
break
return snaps
return snaps, None
@classmethod
def get_gpu_solid_snaps(cls, context, event, objs_to_raycast):
+30 -20
View File
@@ -390,30 +390,40 @@ class Snap(bonsai.core.tool.Snap):
detected_snaps.append(point)
# Objects
objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, objs_2d_bbox)
obj_face_gpu_raycast = tool.Raycast.get_gpu_solid_snaps(context, event, objs_to_raycast)
obj_wireframe_gpu_raycast = tool.Raycast.get_gpu_wireframe_snaps(context, event, objs_to_raycast)
print("FACES", obj_face_gpu_raycast)
print("WIREFRAME", obj_wireframe_gpu_raycast)
closest_snaps = tool.Raycast.ray_cast_and_get_closest_to_camera_snaps(context, event, objs_to_raycast)
detected_snaps.extend(closest_snaps)
xray_mode = (space.shading.type == "SOLID" and space.shading.show_xray) or (
space.shading.type == "WIREFRAME" and space.shading.show_xray_wireframe
)
objs_to_raycast = tool.Raycast.filter_objects_to_raycast(context, event, objs_2d_bbox)
snap_faces, closest_obj = tool.Raycast.get_gpu_solid_snaps(context, event, objs_to_raycast)
wireframe_snaps, _ = tool.Raycast.get_gpu_wireframe_snaps(context, event, objs_to_raycast)
for snap_obj in objs_to_raycast:
for snap in closest_snaps:
if snap_obj == snap["object"]:
if not xray_mode:
# If it is a solid object that is closest to camera it ignores all the rest
if (
"is_closest_to_camera" in snap
and snap["is_closest_to_camera"]
and snap["group"] == "Object"
):
closest_snap = [snap] # discards objects that aren't the closest
detected_snaps = closest_snap
if not xray_mode:
for snap in snap_faces:
if snap["object"] == closest_obj:
detected_snaps.append(snap)
snap_points = tool.Raycast.ray_cast_by_proximity(context, event, snap["object"], snap["object"].data.polygons[snap["face_index"]])
for point in snap_points:
point["group"] = "Object"
detected_snaps.append(point)
# Get wireframe snaps that are not occluded by face snap
ray_origin = tool.Raycast.get_viewport_ray_data(context, event)[0]
occl_dist = (snap["point"] - ray_origin).length + 1e-4
visible_wireframe_snaps = [
w for w in wireframe_snaps
if (w["point"] - ray_origin).length <= occl_dist
]
detected_snaps.extend(visible_wireframe_snaps)
continue
else:
# Doesn't include face snaps, only their edges and vertices
for snap in snap_faces:
snap_points = tool.Raycast.ray_cast_by_proximity(context, event, snap["object"], snap["object"].data.polygons[snap["face_index"]])
for point in snap_points:
point["group"] = "Object"
detected_snaps.append(point)
detected_snaps.extend(wireframe_snaps)
# snap to cut geometry (e.g. in plan view)
if CutDecorator.installed: