diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 7728da27da..3334eb32f2 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -2874,3 +2874,54 @@ class WallSystemPathDecorator(_ConnectedNetworkPathDecorator): else: result.append(point) return result + + +class GpuSnapDecorator(tool.Blender.ViewportDecorator): + """Draw-handler that performs GPU snap detection once per viewport draw. + + The detection work is injected via ``install(..., detection=...)`` so this + decorator has no knowledge of the raycast or snap subsystems. The modal + snapping code sets the current request via :meth:`set_request` and reads the + decoded result from :meth:`get_cache`. + """ + + draw_method = "draw" + + request: dict[str, Any] = {"mouse_x": 0, "mouse_y": 0, "objs_to_raycast": []} + cache: tuple[list[dict], bpy.types.Object | None, list[dict]] | None = None + detection: Any = None + event: Any = None + + @classmethod + def install(cls, context: bpy.types.Context, event=None, detection=None) -> None: + if detection is not None: + cls.detection = detection + cls.event = event + cls.request = {"mouse_x": 0, "mouse_y": 0, "objs_to_raycast": []} + cls.cache = None + super().install(context) + + @classmethod + def uninstall(cls) -> None: + cls.cache = None + cls.event = None + super().uninstall() + + @classmethod + def set_request(cls, event, objs_to_raycast: list[bpy.types.Object]) -> None: + cls.event = event + cls.request["mouse_x"] = int(event.mouse_region_x) + cls.request["mouse_y"] = int(event.mouse_region_y) + cls.request["objs_to_raycast"] = objs_to_raycast + + @classmethod + def get_cache(cls) -> tuple[list[dict], bpy.types.Object | None, list[dict]] | None: + return cls.cache + + def draw(self, context: bpy.types.Context) -> None: + cls = type(self) + if cls.detection is None or cls.event is None or not cls.request["objs_to_raycast"]: + cls.cache = None + return + + cls.cache = cls.detection(context, cls.event, cls.request) diff --git a/src/bonsai/bonsai/bim/module/model/polyline.py b/src/bonsai/bonsai/bim/module/model/polyline.py index 0069961bc0..ad3904ef39 100644 --- a/src/bonsai/bonsai/bim/module/model/polyline.py +++ b/src/bonsai/bonsai/bim/module/model/polyline.py @@ -26,7 +26,7 @@ import ifcopenshell.util.unit from mathutils import Vector import bonsai.tool as tool -from bonsai.bim.module.model.decorator import PolylineDecorator +from bonsai.bim.module.model.decorator import GpuSnapDecorator, PolylineDecorator class PolylineOperator: @@ -424,6 +424,7 @@ class PolylineOperator: def cleanup(self, context: bpy.Types.Context): context.workspace.status_text_set(text=None) PolylineDecorator.uninstall() + GpuSnapDecorator.uninstall() tool.Polyline.clear_polyline() tool.Raycast.clear_cache() tool.Blender.update_viewport() @@ -460,6 +461,7 @@ class PolylineOperator: def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> None: PolylineDecorator.install(context) + GpuSnapDecorator.install(context, event, detection=tool.Raycast.detect_gpu_snaps) tool.Snap.clear_snapping_point() self.tool_state.use_default_container = False diff --git a/src/bonsai/bonsai/tool/raycast.py b/src/bonsai/bonsai/tool/raycast.py index c3bda237c4..bf55f37c7b 100644 --- a/src/bonsai/bonsai/tool/raycast.py +++ b/src/bonsai/bonsai/tool/raycast.py @@ -1108,6 +1108,13 @@ class Raycast(bonsai.core.tool.Raycast): if _offscreen is None: _offscreen = GPUOffScreen(max(w, 1), max(h, 1), format="RGBA8") + # Save GPU state so it can be restored even if readback fails. + prev_depth_mask = gpu.state.depth_mask_get() + prev_depth_test = gpu.state.depth_test_get() + prev_blend = gpu.state.blend_get() + face_culling_get = getattr(gpu.state, "face_culling_get", None) + prev_face_culling = face_culling_get() if face_culling_get is not None else None + _encoding_shader.bind() if xray_mode: @@ -1137,27 +1144,30 @@ class Raycast(bonsai.core.tool.Raycast): buffers_list = [] last_buf = None - with _offscreen.bind(): - fb = gpu.state.active_framebuffer_get() - fb.clear(color=(0.0, 0.0, 0.0, 0.0), depth=1.0) + try: + with _offscreen.bind(): + fb = gpu.state.active_framebuffer_get() + fb.clear(color=(0.0, 0.0, 0.0, 0.0), depth=1.0) - for batch, world_mat, slot_base in render_ops: - mvp = rv3d.perspective_matrix @ world_mat - _encoding_shader.uniform_float("MVP", mvp) - _encoding_shader.uniform_float("slot_base", float(slot_base)) - with gpu.matrix.push_pop(): - gpu.matrix.load_matrix(Matrix.Identity(4)) - batch.draw(_encoding_shader) + for batch, world_mat, slot_base in render_ops: + mvp = rv3d.perspective_matrix @ world_mat + _encoding_shader.uniform_float("MVP", mvp) + _encoding_shader.uniform_float("slot_base", float(slot_base)) + with gpu.matrix.push_pop(): + gpu.matrix.load_matrix(Matrix.Identity(4)) + batch.draw(_encoding_shader) - if read_per_object: # gets all buffers - buf = fb.read_color(int(read_x), int(read_y), read_size, read_size, 4, 0, "UBYTE") - buffers_list.append(buf) - if not read_per_object: - last_buf = fb.read_color(int(read_x), int(read_y), read_size, read_size, 4, 0, "UBYTE") - - # Restore state - gpu.state.depth_mask_set(True) - gpu.state.depth_test_set("LESS") + if read_per_object: # gets all buffers + buf = fb.read_color(int(read_x), int(read_y), read_size, read_size, 4, 0, "UBYTE") + buffers_list.append(buf) + if not read_per_object: + last_buf = fb.read_color(int(read_x), int(read_y), read_size, read_size, 4, 0, "UBYTE") + finally: + gpu.state.depth_mask_set(prev_depth_mask) + gpu.state.depth_test_set(prev_depth_test) + gpu.state.blend_set(prev_blend) + if prev_face_culling is not None: + gpu.state.face_culling_set(prev_face_culling) mouse_read_rect = (w, h, mx, my, read_x, read_y) if tris: @@ -1173,6 +1183,20 @@ class Raycast(bonsai.core.tool.Raycast): def get_gpu_wireframe_snaps(cls, context, event, objs_to_raycast): return cls.get_gpu_detection_snaps(context, event, objs_to_raycast) + @classmethod + def detect_gpu_snaps(cls, context, event, request): + """GPU snap detection callback for ``GpuSnapDecorator``.""" + if bpy.app.background: + return None + + objs_to_raycast = request["objs_to_raycast"] + if not objs_to_raycast: + return None + + solid_snaps, closest_obj = cls.get_gpu_solid_snaps(context, event, objs_to_raycast) + wireframe_snaps, _ = cls.get_gpu_wireframe_snaps(context, event, objs_to_raycast) + return solid_snaps, closest_obj, wireframe_snaps + @classmethod def clear_cache(cls): global _wireframe_batch_cache, _wireframe_vert_fmt, _triangle_batch_cache, _triangle_vert_fmt, _encoding_shader, _offscreen, _obj_list diff --git a/src/bonsai/bonsai/tool/snap.py b/src/bonsai/bonsai/tool/snap.py index 65ae7e0057..b29c23cfe4 100644 --- a/src/bonsai/bonsai/tool/snap.py +++ b/src/bonsai/bonsai/tool/snap.py @@ -30,21 +30,21 @@ import bonsai.core.tool import bonsai.tool as tool from bonsai.bim.module.drawing.data import DecoratorData from bonsai.bim.module.drawing.decoration import CutDecorator -from bonsai.bim.module.model.decorator import PolylineDecorator +from bonsai.bim.module.model.decorator import GpuSnapDecorator, PolylineDecorator if TYPE_CHECKING: from bonsai.bim.prop import BIMSnapGroups, BIMSnapProperties -def _get_gpu_object_snaps( +def _process_gpu_object_snaps( context: bpy.types.Context, event: bpy.types.Event, - objs_to_raycast: list[bpy.types.Object], + snap_faces: list[dict[str, Any]], + closest_obj: bpy.types.Object | None, + wireframe_snaps: list[dict[str, Any]], 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: @@ -85,6 +85,17 @@ def _get_gpu_object_snaps( return detected_snaps +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]]: + 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) + return _process_gpu_object_snaps(context, event, snap_faces, closest_obj, wireframe_snaps, xray_mode) + + def _get_cpu_object_snaps( context: bpy.types.Context, event: bpy.types.Event, @@ -551,7 +562,15 @@ class Snap(bonsai.core.tool.Snap): props = cls.get_snap_props() if props.use_gpu_snapping: - detected_snaps.extend(_get_gpu_object_snaps(context, event, objs_to_raycast, xray_mode)) + GpuSnapDecorator.set_request(event, objs_to_raycast) + cached = GpuSnapDecorator.get_cache() + if cached is None: + detected_snaps.extend(_get_gpu_object_snaps(context, event, objs_to_raycast, xray_mode)) + else: + snap_faces, closest_obj, wireframe_snaps = cached + detected_snaps.extend( + _process_gpu_object_snaps(context, event, snap_faces, closest_obj, wireframe_snaps, xray_mode) + ) else: detected_snaps.extend(_get_cpu_object_snaps(context, event, objs_to_raycast, xray_mode)) cut_snaps = _get_cut_object_snaps(context, event)