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 <noreply@anthropic.com>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-05 09:10:49 +03:00
1 parent 13afc1479f
commit 69bd3c40a9
3 files changed
+75 -7

No files matched your search

+12
View File
@@ -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)
}
+12
View File
@@ -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)
}
+51 -7
View File
@@ -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<Option<HashMap<Handle, Vec<Handle>>>>,
/// 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<Option<bool>>,
/// 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();