feat(scene): Scene::entity_index — lazy quadtree build (Phase 2.1, step 2)
Adds `entity_index_cache: RefCell<Option<(u64, QuadTree)>>` and an accessor that lazily (re)builds on geometry_epoch change. Skips `Insert`/`Viewport`/`Block`/`BlockEnd` and entities whose `bounding_box()` is degenerate or non-finite — those callers must iterate separately. Root bounds derived from the union of indexed entity AABBs with a 1% margin, so the tree is always tight. Still unused; consumers wired in upcoming steps. Cargo emits dead-code warnings for the helpers; they evaporate when the wire/hatch paths switch to `query_rect`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
727992bc84
commit
3e874602e2
1 changed files with 102 additions and 0 deletions
102
src/scene/mod.rs
102
src/scene/mod.rs
|
|
@ -470,6 +470,11 @@ pub struct Scene {
|
|||
/// Lets Insert tessellation transform-copy cached wires instead of
|
||||
/// clone+explode+re-tessellate per reference.
|
||||
block_defn_cache: RefCell<Option<(u64, Arc<block_cache::BlockCache>)>>,
|
||||
/// Spatial index of top-level entity AABBs in WCS f64 (Phase 2.1).
|
||||
/// Keyed by geometry_epoch; lazily rebuilt by `entity_index()`. Skips
|
||||
/// `Insert`/`Viewport`/`Block`/`BlockEnd` and entities with no usable
|
||||
/// bbox — those are handled by special-case "always emit" iteration.
|
||||
entity_index_cache: RefCell<Option<(u64, quadtree::QuadTree)>>,
|
||||
/// Last viewport aspect ratio captured by the render pipeline. Used by
|
||||
/// `view_world_aabb` to compute the world-space view rect on demand.
|
||||
last_render_aspect: std::cell::Cell<f32>,
|
||||
|
|
@ -514,6 +519,7 @@ impl Scene {
|
|||
model_extents_cache: RefCell::new(None),
|
||||
entity_block_map_cache: RefCell::new(None),
|
||||
block_defn_cache: RefCell::new(None),
|
||||
entity_index_cache: RefCell::new(None),
|
||||
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
||||
last_world_per_pixel: std::cell::Cell::new(0.0),
|
||||
}
|
||||
|
|
@ -1410,6 +1416,73 @@ impl Scene {
|
|||
})
|
||||
}
|
||||
|
||||
/// Spatial index of every indexable top-level entity. Lazily built
|
||||
/// on first call after a geometry change; reused across queries
|
||||
/// until `geometry_epoch` advances. Entities filtered out by
|
||||
/// `is_unindexable_entity` (Insert/Viewport/Block/BlockEnd) and
|
||||
/// entities with no usable bbox don't appear here — callers must
|
||||
/// iterate those separately.
|
||||
pub(super) fn entity_index(&self) -> std::cell::Ref<'_, quadtree::QuadTree> {
|
||||
{
|
||||
let cache = self.entity_index_cache.borrow();
|
||||
if let Some((epoch, _)) = *cache {
|
||||
if epoch == self.geometry_epoch {
|
||||
drop(cache);
|
||||
return std::cell::Ref::map(self.entity_index_cache.borrow(), |c| {
|
||||
&c.as_ref().unwrap().1
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 1 — collect (handle, aabb) for every indexable entity.
|
||||
// Union them to derive a tight root bound; anything with a
|
||||
// non-finite or degenerate bbox is skipped here, so the root
|
||||
// bounds stay finite. Wide margin (1%) avoids items sitting
|
||||
// exactly on the root edge.
|
||||
let mut items: Vec<(Handle, [f64; 4])> = Vec::new();
|
||||
let mut union: Option<[f64; 4]> = None;
|
||||
for e in self.document.entities() {
|
||||
if is_unindexable_entity(e) {
|
||||
continue;
|
||||
}
|
||||
let Some(ab) = entity_world_aabb_f64(e) else {
|
||||
continue;
|
||||
};
|
||||
union = Some(match union {
|
||||
None => ab,
|
||||
Some(u) => [
|
||||
u[0].min(ab[0]),
|
||||
u[1].min(ab[1]),
|
||||
u[2].max(ab[2]),
|
||||
u[3].max(ab[3]),
|
||||
],
|
||||
});
|
||||
items.push((e.common().handle, ab));
|
||||
}
|
||||
let root = match union {
|
||||
Some(u) => {
|
||||
let w = (u[2] - u[0]).max(1.0);
|
||||
let h = (u[3] - u[1]).max(1.0);
|
||||
let mx = w * 0.01;
|
||||
let my = h * 0.01;
|
||||
[u[0] - mx, u[1] - my, u[2] + mx, u[3] + my]
|
||||
}
|
||||
// Empty doc — pick a small finite root; query_rect on an
|
||||
// empty tree returns nothing regardless.
|
||||
None => [-1.0, -1.0, 1.0, 1.0],
|
||||
};
|
||||
let mut tree = quadtree::QuadTree::new(root);
|
||||
for (h, ab) in items {
|
||||
tree.insert(h, ab);
|
||||
}
|
||||
|
||||
*self.entity_index_cache.borrow_mut() = Some((self.geometry_epoch, tree));
|
||||
std::cell::Ref::map(self.entity_index_cache.borrow(), |c| {
|
||||
&c.as_ref().unwrap().1
|
||||
})
|
||||
}
|
||||
|
||||
/// Full tessellation pipeline for one entity.
|
||||
fn tessellate_one(&self, e: &EntityType) -> Vec<WireModel> {
|
||||
let bg = if self.current_layout == "Model" {
|
||||
|
|
@ -5061,6 +5134,35 @@ fn entity_aabb(e: &acadrust::EntityType, world_offset: [f64; 3]) -> [f32; 4] {
|
|||
[min_x, min_y, max_x, max_y]
|
||||
}
|
||||
|
||||
/// AABB of `e` in WCS f64 (no world_offset subtraction). `None` for
|
||||
/// entities whose `bounding_box()` returned the degenerate default
|
||||
/// (which `entity_aabb` collapses to `UNBOUNDED_AABB`). Quadtree
|
||||
/// indexing uses this so changing `world_offset` doesn't invalidate
|
||||
/// the index.
|
||||
fn entity_world_aabb_f64(e: &acadrust::EntityType) -> Option<[f64; 4]> {
|
||||
let bbox = e.as_entity().bounding_box();
|
||||
let (xmin, ymin, xmax, ymax) = (bbox.min.x, bbox.min.y, bbox.max.x, bbox.max.y);
|
||||
if xmin == xmax && ymin == ymax {
|
||||
return None;
|
||||
}
|
||||
if !xmin.is_finite() || !ymin.is_finite() || !xmax.is_finite() || !ymax.is_finite() {
|
||||
return None;
|
||||
}
|
||||
Some([xmin, ymin, xmax, ymax])
|
||||
}
|
||||
|
||||
/// True if `e` is a type the quadtree should skip. `Insert` and
|
||||
/// `Viewport` are sized only after extra transformation; tessellation
|
||||
/// already handles them via dedicated code paths. `Block`/`BlockEnd`
|
||||
/// are block-defn sentinels with no geometry.
|
||||
fn is_unindexable_entity(e: &acadrust::EntityType) -> bool {
|
||||
use acadrust::EntityType as E;
|
||||
matches!(
|
||||
e,
|
||||
E::Insert(_) | E::Viewport(_) | E::Block(_) | E::BlockEnd(_)
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate solid-fill boundary polygons for each wide segment of an LWPolyline.
|
||||
/// Returns one polygon per segment that has non-zero width; empty if the polyline
|
||||
/// has zero `constant_width` and all vertex widths are zero.
|
||||
|
|
|
|||
Loading…
Reference in a new issue