From e43498d6c410a319c0283fd3f8b6a3d6aba9f77d Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 17 May 2026 14:46:51 +0300 Subject: [PATCH] perf(scene): quadtree culling in wire tessellation (Phase 2.1, step 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `wires_for_block` now drives candidate selection from the quadtree when a Model-layout view AABB is available — `query_rect` replaces the O(N) `doc.entities().filter(...)` scan. Visibility predicate is hoisted into a closure and reused for the quadtree-candidate path and the unindexable-entity append (Insert/Viewport). Paper space and the pre-fit first-frame settle path keep the original full-scan behaviour (no view AABB → no cull). Per-entity LOD + frustum checks inside `tessellate_entity` stay in place; the quadtree is purely a candidate reducer so culling correctness still funnels through one place. Co-Authored-By: Claude Opus 4.7 --- src/scene/mod.rs | 87 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 61 insertions(+), 26 deletions(-) diff --git a/src/scene/mod.rs b/src/scene/mod.rs index c8e0d2adf..2342556e1 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1235,34 +1235,69 @@ impl Scene { } } - // Collect visible entities sequentially (filter needs &self). - let visible: Vec<&EntityType> = self - .document - .entities() - .filter(|e| { - let c = e.common(); - if c.invisible { - return false; + // Visibility test reused by both paths below. + let visibility_ok = |e: &EntityType| -> bool { + let c = e.common(); + if c.invisible { + return false; + } + // Block/BlockEnd are block-defn sentinels, not drawable geometry. + // Without this skip they fall through to legacy_geometry's `_` + // arm and emit a 1-unit phantom segment at world_offset that + // poisons fit_all and shows up in selection. + if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) { + return false; + } + if self + .document + .layers + .get(&c.layer) + .map(|l| l.flags.off || l.flags.frozen) + .unwrap_or(false) + { + return false; + } + self.belongs_to_visible_block(e.common().handle, c.owner_handle, block_handle) + }; + + // Phase 2.1 — quadtree-driven candidate selection. When a view + // AABB exists (Model layout with a settled camera), only iterate + // entities whose stored WCS bbox intersects the view; unindexable + // entities (Insert/Viewport) are appended via a small linear scan. + // Paper space and the first-frame "settle" path fall back to the + // full doc scan — preserving prior behaviour. + let visible: Vec<&EntityType> = if let Some(local_view) = self.view_world_aabb() { + let [ox, oy, _] = self.world_offset; + let view_wcs: [f64; 4] = [ + local_view[0] as f64 + ox, + local_view[1] as f64 + oy, + local_view[2] as f64 + ox, + local_view[3] as f64 + oy, + ]; + let candidates: Vec = { + let tree = self.entity_index(); + tree.query_rect(view_wcs) + }; + let mut out: Vec<&EntityType> = Vec::with_capacity(candidates.len() + 16); + for h in candidates { + if let Some(e) = self.document.get_entity(h) { + if visibility_ok(e) { + out.push(e); + } } - // Block/BlockEnd are block-defn sentinels, not drawable geometry. - // Without this skip they fall through to legacy_geometry's `_` - // arm and emit a 1-unit phantom segment at world_offset that - // poisons fit_all and shows up in selection. - if matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_)) { - return false; + } + for e in self.document.entities() { + if is_unindexable_entity(e) && visibility_ok(e) { + out.push(e); } - if self - .document - .layers - .get(&c.layer) - .map(|l| l.flags.off || l.flags.frozen) - .unwrap_or(false) - { - return false; - } - self.belongs_to_visible_block(e.common().handle, c.owner_handle, block_handle) - }) - .collect(); + } + out + } else { + self.document + .entities() + .filter(|e| visibility_ok(e)) + .collect() + }; // Tessellate in parallel across all available CPU cores. use rayon::prelude::*;