From c333889fc016eb6abe203b0f679cb627c172b358 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Tue, 13 Oct 2026 19:45:18 -0400 Subject: [PATCH] Open large designs faster: index the previous bodies by name after each feature (#912) After each feature the bodies before it are indexed by name once (first match first, as the linear searches were), feature components come from a map built once per evaluation, and Body::same compares shared face sources, paint and split lines by pointer first. --- crates/doc/src/eval.rs | 27 ++++++++++++++++++++++----- crates/kernel/src/body.rs | 15 ++++++++++++--- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index 8a5ca8c5..1ab8c824 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -477,15 +477,23 @@ impl ModelState { if !f.shell_piece_order { crate::lineage::order_lumps(&mut self.bodies, &origin); } + // The bodies before the feature by name, in their order (names repeat only in damaged + // states): every lookup below is the first match of a linear search, without walking + // every earlier body for every body, after every feature. + let mut by_name: std::collections::HashMap<&str, Vec<&ModelBody>> = std::collections::HashMap::with_capacity(before.bodies.len()); + for old in &before.bodies { + by_name.entry(old.name.as_str()).or_default().push(old); + } for b in &mut self.bodies { + let olds: &[&ModelBody] = by_name.get(b.name.as_str()).map(Vec::as_slice).unwrap_or_default(); if !matches!(f.kind, FeatureKind::BodyTransfer { .. }) - && let Some(old) = before.bodies.iter().find(|old| old.name == b.name && old.transferred) + && let Some(old) = olds.iter().find(|old| old.transferred) { b.transferred = true; b.component = old.component; b.source_transform = old.source_transform; } - let kept = before.bodies.iter().find(|x| x.name == b.name && x.body.same(&b.body)); + let kept = olds.iter().find(|x| x.body.same(&b.body)); match kept { Some(x) => { b.key = x.key; @@ -501,14 +509,14 @@ impl ModelState { } // Faces split on purpose stay split through later features (for export). if b.body.split_lines().is_none() - && let Some(lines) = before.bodies.iter().find(|x| x.name == b.name).and_then(|x| x.body.split_lines()) + && let Some(lines) = olds.first().and_then(|x| x.body.split_lines()) { b.body = b.body.clone().with_split_lines(Some(lines.clone())); } // An imported look outlives the operations on the body. if b.body.paint().is_none() && b.body.color().is_none() - && let Some(old) = before.bodies.iter().find(|x| x.name == b.name) + && let Some(old) = olds.first() { b.imported = crate::appearance::ImportedLook::of(old); } @@ -1001,6 +1009,12 @@ impl Model { let mut restored = 0; let marker = doc.marker.unwrap_or(usize::MAX); let ctx = context_digest(doc, &vals); + // Each feature's component, for `Document::legacy_body_component` on every body after + // every feature without searching the timeline for each (the first feature with an id). + let mut feature_component: std::collections::HashMap = std::collections::HashMap::with_capacity(doc.features.len()); + for f in &doc.features { + feature_component.entry(f.id).or_insert(f.component); + } let watched = crate::progress::begin(doc.features.len()); for (i, f) in doc.features.iter().enumerate() { let rolled_back = i >= marker; @@ -1143,7 +1157,10 @@ impl Model { next.stamp(&state, f, key, mats); for b in &mut next.bodies { if !b.transferred { - b.component = doc.legacy_body_component(&b.name, b.feature); + b.component = match doc.body_components.get(&b.name) { + Some(c) => *c, + None => feature_component.get(&b.feature).copied().unwrap_or(0), + }; } } state = Arc::new(next); diff --git a/crates/kernel/src/body.rs b/crates/kernel/src/body.rs index a0e9710f..d9852844 100644 --- a/crates/kernel/src/body.rs +++ b/crates/kernel/src/body.rs @@ -467,6 +467,15 @@ fn span_of(solid: &Solid) -> f64 { if lo.x.is_finite() { (hi - lo).len() } else { 1.0 } } +/// Equal shared values: the same allocation (a clone of the body), else equal contents. +fn same_shared(a: &Option>, b: &Option>) -> bool { + match (a, b) { + (Some(a), Some(b)) => std::sync::Arc::ptr_eq(a, b) || a == b, + (None, None) => true, + _ => false, + } +} + impl Body { /// The very same body (shared data), not just an equal-looking one. Its history (how an /// operation made it) is not part of it: an operation that leaves a body as it is (a cut by @@ -479,9 +488,9 @@ impl Body { _ => false, } && self.color == other.color - && self.paint == other.paint - && self.splits == other.splits - && self.face_sources == other.face_sources + && same_shared(&self.paint, &other.paint) + && same_shared(&self.splits, &other.splits) + && same_shared(&self.face_sources, &other.face_sources) && self.regularization_protected == other.regularization_protected }