diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 9c38d148c..2a151ba04 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1848,6 +1848,13 @@ pub struct Scene { /// `belongs_to_visible_block` treats as belonging to everything — but only /// when no block record enumerates its contents at all. block_members_cache: RefCell>, + /// Handles of entities the quadtree cannot index — Insert, Viewport, Block, + /// BlockEnd — in document order, keyed by `geometry_epoch`. + /// + /// The quadtree path has to append these regardless of the view, and did so + /// by walking every entity in the document: ~300 ms of every resident + /// rebuild on a 1.17 M-entity drawing, to find a few thousand. + unindexable_cache: RefCell)>>, /// Distinct entity type names present in a layout, keyed by /// (geometry_epoch, layout block). The status bar asks for this on every /// frame to populate the selection-filter menu, but the answer only @@ -2093,6 +2100,7 @@ impl Scene { model_extents_cache: RefCell::new(None), entity_block_map_cache: RefCell::new(None), block_members_cache: RefCell::new(None), + unindexable_cache: RefCell::new(None), layout_type_names_cache: RefCell::new(None), dependency_index_cache: RefCell::new(None), associative_hatch_source_cache: RefCell::new(None), @@ -8902,7 +8910,7 @@ impl Scene { let sort_cache_ms = crate::perf::elapsed_ms(t_fn); let t_visible = perf.then(iced::time::Instant::now); // Which path the selection took, and how many candidates it looked at. - let mut visible_path = "quadtree"; + let mut visible_path = "quadtree+index"; let mut visible_candidates = 0usize; // Phase 2.1 — quadtree-driven candidate selection. When a view @@ -8941,10 +8949,19 @@ impl Scene { } } // Inserts/Viewports/Block/BlockEnd — handled by their own paths - // (block expansion, viewport rendering); always candidates. - for e in self.document.entities() { - if is_unindexable_entity(e) && visibility_ok(e) { - out.push(e); + // (block expansion, viewport rendering); always candidates. Taken + // from the per-epoch list rather than by walking the document, + // which preserves document order because the list is built that + // way. + { + let unindexable = self.unindexable_handles(); + visible_candidates = unindexable.1.len(); + for &h in &unindexable.1 { + if let Some(e) = self.document.get_entity(h) { + if visibility_ok(e) { + out.push(e); + } + } } } out @@ -9239,6 +9256,29 @@ entities={} memo_hit={} memo_miss={} wires={} memo={} visible_path={} candidates /// * an entity neither names nor is named by any block is "unowned", and /// counts towards every block only when no block record enumerates its /// contents at all — the legacy-DXF case that predicate allows. + /// Entities the quadtree cannot index, in document order, resolved once per + /// `geometry_epoch`. Membership depends only on the entity's type, so an + /// edit that does not add or remove one leaves the list unchanged. + fn unindexable_handles(&self) -> std::cell::Ref<'_, (u64, Vec)> { + { + let cache = self.unindexable_cache.borrow(); + if cache.as_ref().is_some_and(|(epoch, _)| *epoch == self.geometry_epoch) { + drop(cache); + return std::cell::Ref::map(self.unindexable_cache.borrow(), |c| { + c.as_ref().unwrap() + }); + } + } + let handles: Vec = self + .document + .entities() + .filter(|entity| is_unindexable_entity(entity)) + .map(|entity| entity.common().handle) + .collect(); + *self.unindexable_cache.borrow_mut() = Some((self.geometry_epoch, handles)); + std::cell::Ref::map(self.unindexable_cache.borrow(), |c| c.as_ref().unwrap()) + } + fn block_members(&self) -> std::cell::Ref<'_, BlockMembers> { { let cache = self.block_members_cache.borrow();