perf(scene): quadtree culling in hatch + wipeout passes (Phase 2.1, step 4)

`synced_hatch_models` and `wipeout_models` now pre-filter their
per-entity scans through `entity_index().query_rect(view)` when a
Model-layout view AABB is available. Affects:

- Top-level hatch HashMap iteration (the dominant cost — clone +
  render_style + selection tint per hatch).
- Wide LWPolyline / Polyline2D solid-fill loop.
- Wipeout entity scan.

The Insert→exploded-hatch loop is left alone; Insert is not
quadtree-indexed (its WCS bbox depends on block defn × insert
transform, handled inside block_cache).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-17 14:50:00 +03:00
commit 5b8cdd940e

View file

@ -2474,10 +2474,33 @@ impl Scene {
.unwrap_or(false) .unwrap_or(false)
}; };
// Phase 2.1 — narrow the per-hatch visibility scan to candidates
// the quadtree says intersect the view. The map below still does
// the heavy lifting (model clone + render_style + selection tint)
// so cutting the pre-filter from O(N_hatches) → O(visible_hatches)
// is the main win.
let view_candidates: Option<std::collections::HashSet<Handle>> =
self.view_world_aabb().map(|local_view| {
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 tree = self.entity_index();
tree.query_rect(view_wcs).into_iter().collect()
});
let mut models: Vec<HatchModel> = self let mut models: Vec<HatchModel> = self
.hatches .hatches
.iter() .iter()
.filter(|(&handle, _)| { .filter(|(&handle, _)| {
if let Some(set) = view_candidates.as_ref() {
if !set.contains(&handle) {
return false;
}
}
let Some(entity) = self.document.get_entity(handle) else { let Some(entity) = self.document.get_entity(handle) else {
return true; return true;
}; };
@ -2562,6 +2585,11 @@ impl Scene {
if fills.is_empty() { if fills.is_empty() {
continue; continue;
} }
if let Some(set) = view_candidates.as_ref() {
if !set.contains(&common.handle) {
continue;
}
}
if common.invisible || layer_hidden(&common.layer) { if common.invisible || layer_hidden(&common.layer) {
continue; continue;
} }
@ -2607,11 +2635,29 @@ impl Scene {
} else { } else {
self.world_offset self.world_offset
}; };
// Phase 2.1 — quadtree pre-filter for wipeouts in Model layout.
let view_candidates: Option<std::collections::HashSet<Handle>> =
self.view_world_aabb().map(|local_view| {
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 tree = self.entity_index();
tree.query_rect(view_wcs).into_iter().collect()
});
let mut models = Vec::new(); let mut models = Vec::new();
for entity in self.document.entities() { for entity in self.document.entities() {
let EntityType::Wipeout(wo) = entity else { let EntityType::Wipeout(wo) = entity else {
continue; continue;
}; };
if let Some(set) = view_candidates.as_ref() {
if !set.contains(&wo.common.handle) {
continue;
}
}
if entity.common().invisible { if entity.common().invisible {
continue; continue;
} }