From 69bd3c40a9ed3c0a2e412cc5716ef52b5e300033 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:16:24 +0300 Subject: [PATCH] perf: stop scanning every entity for associative centers on each edit The spans put it exactly: 63.5 ms of a 63.8 ms pointer-up is `bump_entities`, and inside it two drawing-wide scans. `refresh_associative_centerlines` and `refresh_associative_center_marks` each walk all 1 166 357 entities, filter to Lines and parse every one's extended data looking for an association that references a changed handle. About 30 ms each, run on every edit -- to find, on this drawing, none at all. Track whether the document holds any such association. The question is settled once, with a single pass, and after that each edit only examines the handles it actually changed: an entity that carries an association flips the answer to yes, and nothing else can. Deleting the last one leaves the answer at yes, which costs a scan that finds nothing rather than skipping one it should have run. A coarse `bump_geometry` re-opens the question, since it can carry a new document. Where an association really exists the scans run exactly as before. Co-Authored-By: Claude Opus 5 --- src/scene/centerline.rs | 12 +++++++++ src/scene/centermark.rs | 12 +++++++++ src/scene/mod.rs | 58 ++++++++++++++++++++++++++++++++++++----- 3 files changed, 75 insertions(+), 7 deletions(-) diff --git a/src/scene/centerline.rs b/src/scene/centerline.rs index 2438506a7..ecb735cd4 100644 --- a/src/scene/centerline.rs +++ b/src/scene/centerline.rs @@ -435,3 +435,15 @@ pub(crate) fn center_measure_is_relative(specification: &str) -> bool { let text = specification.trim(); text.eq_ignore_ascii_case("ByLineType") || text.ends_with(['x', 'X']) } + +/// Whether `entity` carries a centerline association. +/// +/// Used to keep the drawing-wide scan off the edit path: only an entity that +/// actually carries one can make the scan necessary. +pub(crate) fn entity_has_center_association(entity: &EntityType) -> bool { + let EntityType::Line(line) = entity else { + return false; + }; + CenterLineAssociation::read(&line.common.extended_data) + .is_some_and(|association| association.associated) +} diff --git a/src/scene/centermark.rs b/src/scene/centermark.rs index fd719775b..979ed0741 100644 --- a/src/scene/centermark.rs +++ b/src/scene/centermark.rs @@ -473,3 +473,15 @@ impl Scene { candidates.len() } } + +/// Whether `entity` carries a center mark association. +/// +/// Used to keep the drawing-wide scan off the edit path: only an entity that +/// actually carries one can make the scan necessary. +pub(crate) fn entity_has_center_association(entity: &EntityType) -> bool { + let EntityType::Line(line) = entity else { + return false; + }; + CenterMarkAssociation::read(&line.common.extended_data) + .is_some_and(|association| association.associated) +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index da511e24f..c3d6f2b71 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1827,6 +1827,15 @@ pub struct Scene { /// Boundary source → associative hatch handles. Source edits reuse this /// index instead of scanning the document during every drag step. associative_hatch_source_cache: RefCell>>>, + /// Whether the drawing contains any associative centerline or center mark. + /// + /// Refreshing those means scanning every entity and parsing its extended + /// data — 1.17 M entities and ~30 ms each on the reproducer, run twice on + /// every edit, to find none at all. `None` means not yet determined; the + /// first scan records the answer, and later edits only re-examine the + /// handles they actually changed. Staying `true` after the last one is + /// deleted is merely slower, never wrong. + has_associative_centers: std::cell::Cell>, /// Tessellated block definitions in block-local coords, keyed by render /// background and block epoch. Model and Paper adapt black/white colours /// differently; retaining both variants prevents a full block rebuild on @@ -2057,6 +2066,7 @@ impl Scene { layout_type_names_cache: RefCell::new(None), dependency_index_cache: RefCell::new(None), associative_hatch_source_cache: RefCell::new(None), + has_associative_centers: std::cell::Cell::new(None), block_defn_cache: RefCell::new(HashMap::default()), entity_index_cache: RefCell::new(None), last_render_aspect: std::cell::Cell::new(16.0 / 9.0), @@ -2531,6 +2541,35 @@ impl Scene { changes } + /// Whether the associative centerline / center-mark scans can be skipped. + /// + /// Answers from the cached hint when it is known, and otherwise from the + /// changed entities alone: if any of them carries an association the hint + /// flips to `true` and the scans run from then on. Only an undetermined + /// hint costs a full pass, and that happens once. + fn associative_centers_possible(&self, changes: &[(Handle, ChangeKind)]) -> bool { + if changes.iter().any(|(handle, _)| { + self.document.get_entity(*handle).is_some_and(|entity| { + crate::scene::centerline::entity_has_center_association(entity) + || crate::scene::centermark::entity_has_center_association(entity) + }) + }) { + self.has_associative_centers.set(Some(true)); + return true; + } + if let Some(known) = self.has_associative_centers.get() { + return known; + } + // Undetermined: settle it with one pass over the drawing, then never + // again for this document. + let any = self.document.entities().any(|entity| { + crate::scene::centerline::entity_has_center_association(entity) + || crate::scene::centermark::entity_has_center_association(entity) + }); + self.has_associative_centers.set(Some(any)); + any + } + pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) { if changes.iter().any(|(handle, kind)| { matches!(kind, ChangeKind::Removed) @@ -2542,14 +2581,16 @@ impl Scene { self.associative_hatch_source_cache.borrow_mut().take(); } let mut changes = changes.to_vec(); - for change in self.refresh_associative_centerlines(&changes) { - if !changes.iter().any(|(handle, _)| *handle == change.0) { - changes.push(change); + if self.associative_centers_possible(&changes) { + for change in self.refresh_associative_centerlines(&changes) { + if !changes.iter().any(|(handle, _)| *handle == change.0) { + changes.push(change); + } } - } - for change in self.refresh_associative_center_marks(&changes) { - if !changes.iter().any(|(handle, _)| *handle == change.0) { - changes.push(change); + for change in self.refresh_associative_center_marks(&changes) { + if !changes.iter().any(|(handle, _)| *handle == change.0) { + changes.push(change); + } } } for change in self.refresh_associative_dimensions(&changes) { @@ -2662,6 +2703,9 @@ impl Scene { // Default: also invalidate block definitions. Safe for every caller; // operations that know blocks are untouched use `bump_geometry_no_blocks`. self.block_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed); + // A change this coarse can carry a new document; re-determine whether + // it holds associative centerlines or center marks. + self.has_associative_centers.set(None); // Structural change — drop both per-entity tessellation memos. self.tess_memo.borrow_mut().clear(); self.resident_tess_memo.borrow_mut().clear();