snap: add option to use GPU snapping. It is disabled by default

This commit is contained in:
Bruno Perdigão
2026-08-23 12:42:42 -03:00
parent 144696c8b9
commit ae9f7bfeb6
3 changed files with 166 additions and 38 deletions
+2
View File
@@ -829,6 +829,7 @@ class BIMSnapGroups(PropertyGroup):
class BIMSnapProperties(PropertyGroup):
use_gpu_snapping: BoolProperty(name="Use GPU Snapping", default=False)
vertex: BoolProperty(name="Vertex", default=True)
edge: BoolProperty(name="Edge", default=True)
edge_center: BoolProperty(name="Edge Center", default=True)
@@ -836,6 +837,7 @@ class BIMSnapProperties(PropertyGroup):
face: BoolProperty(name="Face", default=True)
if TYPE_CHECKING:
use_gpu_snapping: bool
vertex: bool
edge: bool
edge_center: bool
+3
View File
@@ -2071,3 +2071,6 @@ class BIM_PT_snappping(Panel):
row.prop(groups, "object", toggle=True)
row.prop(groups, "polyline", toggle=True)
row.prop(groups, "measure", toggle=True)
layout.separator()
row = layout.row(align=True)
row.prop(prop, "use_gpu_snapping", toggle=True)
+161 -38
View File
@@ -36,6 +36,160 @@ if TYPE_CHECKING:
from bonsai.bim.prop import BIMSnapGroups, BIMSnapProperties
def _get_gpu_object_snaps(
context: bpy.types.Context,
event: bpy.types.Event,
objs_to_raycast: list[bpy.types.Object],
xray_mode: bool,
) -> list[dict[str, Any]]:
detected_snaps: list[dict[str, Any]] = []
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)
if not xray_mode:
for snap in snap_faces:
if snap["object"] == closest_obj:
detected_snaps.append(snap)
# Tris from cut geometry are irregular, so we don't want to use them
# to create edges and vertices snaps. They are handled by wireframe snaps
if not snap["is_cut"]:
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
if not snap_faces:
detected_snaps.extend(wireframe_snaps)
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)
return detected_snaps
def _get_cpu_object_snaps(
context: bpy.types.Context,
event: bpy.types.Event,
objs_to_raycast: list[bpy.types.Object],
xray_mode: bool,
) -> list[dict[str, Any]]:
detected_snaps: list[dict[str, Any]] = []
closest_snaps = tool.Raycast.ray_cast_and_get_closest_to_camera_snaps(context, event, objs_to_raycast)
wireframe_snaps = [snap for snap in closest_snaps if snap["group"] == "Wireframe"]
face_snaps = [snap for snap in closest_snaps if snap["group"] == "Object"]
if not xray_mode:
closest_face_snap = None
for snap in face_snaps:
if snap.get("is_closest_to_camera"):
closest_face_snap = snap
break
if closest_face_snap:
detected_snaps.append(closest_face_snap)
snap_points = tool.Raycast.ray_cast_by_proximity(
context,
event,
closest_face_snap["object"],
closest_face_snap["object"].data.polygons[closest_face_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 = (closest_face_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)
else:
detected_snaps.extend(wireframe_snaps)
else:
# Doesn't include face snaps, only their edges and vertices
for snap in face_snaps:
if snap["face_index"] is None:
continue
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)
return detected_snaps
def _get_cut_object_snaps(
context: bpy.types.Context,
event: bpy.types.Event,
) -> list[dict[str, Any]]:
if not CutDecorator.installed:
return []
cut_snaps: list[dict[str, Any]] = []
model_props = tool.Model.get_model_props()
for obj in [o for o in context.visible_objects if o.type == "MESH"]:
if not (element := tool.Ifc.get_entity(obj)):
continue
if model_props.show_cut_decorator and element.id() in DecoratorData.cut_cache:
verts, edges = DecoratorData.cut_cache[element.id()]
if not verts or not edges:
continue
bm = bmesh.new()
bverts = [bm.verts.new(pos) for pos in verts]
for edge in edges:
bm.edges.new([bverts[vi] for vi in edge])
snap_points = tool.Raycast.ray_cast_by_proximity(context, event, None, None, bm)
if snap_points:
for p in snap_points:
p["group"] = "Object"
p["object"] = obj
cut_snaps.append(p)
if model_props.show_cut_decorator_fill and element.id() in DecoratorData.fill_cache:
bm = bmesh.new()
for color, verts_and_tris in DecoratorData.fill_cache[element.id()].items():
for verts, tris in verts_and_tris:
bverts = [bm.verts.new(pos) for pos in verts]
for tri in tris:
verts = [bverts[vi] for vi in tri]
if not bm.faces.get(verts):
bm.faces.new(verts)
snap_points = tool.Raycast.ray_cast_by_proximity(context, event, None, None, bm)
if snap_points:
for p in snap_points:
p["group"] = "Object"
p["object"] = obj
cut_snaps.append(p)
return cut_snaps
class Snap(bonsai.core.tool.Snap):
tool_state = None
snap_plane_method = None
@@ -394,46 +548,15 @@ class Snap(bonsai.core.tool.Snap):
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)
if not xray_mode:
for snap in snap_faces:
if snap["object"] == closest_obj:
detected_snaps.append(snap)
# Tris from cut geometry are irregular, so we don't want to use them
# to create edges and vertices snaps. They are handled by wireframe snaps
if not snap["is_cut"]:
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
if not snap_faces:
detected_snaps.extend(wireframe_snaps)
props = cls.get_snap_props()
if props.use_gpu_snapping:
detected_snaps.extend(_get_gpu_object_snaps(context, event, objs_to_raycast, xray_mode))
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)
detected_snaps.extend(_get_cpu_object_snaps(context, event, objs_to_raycast, xray_mode))
cut_snaps = _get_cut_object_snaps(context, event)
if cut_snaps:
detected_snaps = cut_snaps
# Axis and Plane
if tool.Ifc.get():