mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Designs before format 14: a split's pieces take the names its later features use them by
Builds before format 14 named the pieces a cut splits a body into in truck's shell order, and the later features of such a design name the piece they work on by that name. SMLib orders the pieces otherwise, so split-body-pieces' fillet and chamfer landed on the wrong piece, its reference "no longer exists" (design_corpus body_names_saved_before_exact_history_keep_their_pieces, old_designs_open_and_evaluate_as_saved). For a feature saved so (`shell_piece_order`), each name its pieces are known by goes to the piece the points the later features naming it picked lie on; a name no later feature uses keeps SMLib's order, and so do all of them where the picks do not tell the pieces apart. docs/compatibility.md says so. That makes the feature's result depend on features after it, which neither its fingerprint nor its cache key cover: a script replays the design one feature at a time, so the split was evaluated before the fillet and chamfer existed, kept SMLib's order, and was reused once they came (script_roundtrip corpus_designs_replay_from_their_scripts: 'Body1 (1)' 4765.663706234679 opened against 4765.663706134907 replayed). The state now keeps a digest of the picks it named the pieces by (`ModelState::legacy_picks`), and every reuse (the last results, the cache, the kept, rolled-back and primed results) takes a state for such a feature only while the design's later features pick the same, so the replay names the pieces again as the file does when it opens. A saved model's states carry no lineage and are taken as saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+37
-5
@@ -535,6 +535,10 @@ pub struct ModelState {
|
||||
/// How the pieces a join split off into bodies of their own touched the others (by feature
|
||||
/// id), for its notice.
|
||||
kept_separate: BTreeMap<u64, KeptSeparate>,
|
||||
/// For a feature saved before format 14 that split a body, a digest of the later features'
|
||||
/// picks its pieces were named by (by feature id, see [`crate::lineage::order_legacy_lumps`]):
|
||||
/// the state stands for the feature only while those picks are the same.
|
||||
legacy_picks: BTreeMap<u64, u64>,
|
||||
}
|
||||
|
||||
/// How a piece a join split off into a body of its own (`Touching::Separate`) touched the
|
||||
@@ -645,7 +649,7 @@ impl ModelState {
|
||||
/// After a feature evaluated to this state from `before` with cache key `key`: bodies it
|
||||
/// made or changed take that key, sketches it made a digest of their solved geometry (see
|
||||
/// [`quantize`]); the rest keep theirs.
|
||||
fn stamp(&mut self, before: &Arc<ModelState>, f: &Feature, key: u64, mats: Vec<Mat>) {
|
||||
fn stamp(&mut self, doc: &Document, before: &Arc<ModelState>, f: &Feature, key: u64, mats: Vec<Mat>) {
|
||||
let feature = f.id;
|
||||
let depth = before.bodies.iter().filter_map(|b| b.names.depth()).max().unwrap_or(0) + 1;
|
||||
let sheets = if f.kind.is_sheet() { self.sheets.clone() } else { Vec::new() };
|
||||
@@ -654,7 +658,13 @@ impl ModelState {
|
||||
// the feature was saved when they were named in that order).
|
||||
if !f.shell_piece_order {
|
||||
crate::lineage::order_lumps(&mut self.bodies, &origin);
|
||||
} else {
|
||||
crate::lineage::order_legacy_lumps(&mut self.bodies, before, doc, f);
|
||||
}
|
||||
match crate::lineage::legacy_picks_digest(&self.bodies, before, doc, f) {
|
||||
0 => self.legacy_picks.remove(&f.id),
|
||||
d => self.legacy_picks.insert(f.id, d),
|
||||
};
|
||||
// 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.
|
||||
@@ -1328,6 +1338,15 @@ fn context_digest(doc: &Document, vals: &BTreeMap<String, Value>) -> u64 {
|
||||
h.finish()
|
||||
}
|
||||
|
||||
/// Whether `after`, a state feature `f` evaluated to from `before` in an earlier evaluation, is
|
||||
/// still its result in `doc`: a feature saved before format 14 names the pieces it splits a body
|
||||
/// into as the later features pick them ([`crate::lineage::order_legacy_lumps`]), which neither
|
||||
/// its fingerprint nor its cache key cover. (A script replays those features after it: the state
|
||||
/// made before they were there named the pieces otherwise.)
|
||||
fn legacy_current(doc: &Document, f: &Feature, before: &ModelState, after: &ModelState) -> bool {
|
||||
!f.shell_piece_order || after.legacy_picks.get(&f.id).copied().unwrap_or(0) == crate::lineage::legacy_picks_digest(&after.bodies, before, doc, f)
|
||||
}
|
||||
|
||||
/// The cache key of a feature evaluation: its own inputs, the document context and the state it
|
||||
/// starts from. Features that replay other features' definitions (patterns and mirrors of
|
||||
/// features) also take the chain fingerprint, which covers every earlier definition.
|
||||
@@ -1860,9 +1879,11 @@ impl Model {
|
||||
if f.suppressed && !rolled_back {
|
||||
reserved.extend(f.body_names.iter().map(|n| n.trim()).filter(|n| !n.is_empty()).map(str::to_string));
|
||||
}
|
||||
// A feature that names its pieces as later features pick them (saved before format 14)
|
||||
// has a result only while those picks are what they were.
|
||||
if reuse
|
||||
&& !unkept
|
||||
&& let Some(old) = self.results.get(i).filter(current)
|
||||
&& let Some(old) = self.results.get(i).filter(current).filter(|old| old.skipped || legacy_current(doc, f, &state, &old.state))
|
||||
{
|
||||
if !old.skipped {
|
||||
state = old.state.clone();
|
||||
@@ -1898,7 +1919,13 @@ impl Model {
|
||||
cache_key(f, fp, fingerprint(doc, 0, f, &vals, false, &self.step_digests) ^ placement_digest(doc, f, Some(&state)), ctx, &state);
|
||||
let kept = self.kept.0.as_ref().filter(|_| !self.no_cache);
|
||||
if !self.no_cache
|
||||
&& let Some(c) = self.cache.lock().ok().and_then(|mut m| m.get(key)).or_else(|| kept.and_then(|k| k.get(key)))
|
||||
&& let Some(c) = self
|
||||
.cache
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut m| m.get(key))
|
||||
.or_else(|| kept.and_then(|k| k.get(key)))
|
||||
.filter(|c| c.error.is_some() || legacy_current(doc, f, &state, &c.state))
|
||||
{
|
||||
if let Some(k) = kept {
|
||||
k.put(key, &c);
|
||||
@@ -1927,7 +1954,12 @@ impl Model {
|
||||
}
|
||||
// An active result hidden by the marker, or evaluated ahead on these same inputs
|
||||
// (the same chain of definitions from the start). Changed inputs never reuse it.
|
||||
if let Some(r) = rolled.as_ref().filter(current).or_else(|| primed.as_ref().and_then(|p| p.get(&f.id)).filter(current)) {
|
||||
if let Some(r) = rolled
|
||||
.as_ref()
|
||||
.filter(current)
|
||||
.or_else(|| primed.as_ref().and_then(|p| p.get(&f.id)).filter(current))
|
||||
.filter(|r| r.error.is_some() || legacy_current(doc, f, &state, &r.state))
|
||||
{
|
||||
restored += 1;
|
||||
if r.error.is_none() {
|
||||
state = r.state.clone();
|
||||
@@ -2010,7 +2042,7 @@ impl Model {
|
||||
}
|
||||
_ => mats,
|
||||
};
|
||||
next.stamp(&state, f, key, mats);
|
||||
next.stamp(doc, &state, f, key, mats);
|
||||
for b in &mut next.bodies {
|
||||
if !b.transferred {
|
||||
b.component = match doc.body_components.get(&b.name) {
|
||||
|
||||
+145
-1
@@ -11,7 +11,7 @@
|
||||
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
use solidcraft_geom::Region2;
|
||||
use solidcraft_geom::{Region2, Vec3};
|
||||
use solidcraft_kernel::Body;
|
||||
use solidcraft_kernel::history::{BodyHistory, Change, FaceOrigin, FaceProv, History};
|
||||
|
||||
@@ -568,6 +568,150 @@ impl crate::ModelBody {
|
||||
}
|
||||
}
|
||||
|
||||
/// The pieces feature `f` (saved before format 14) split a body into, named so the later
|
||||
/// features of the design find them. Those builds named the pieces in truck's shell order, which
|
||||
/// SMLib's need not follow, and a later feature names the piece it works on (`body`) and keeps
|
||||
/// the points it picked on it: each name goes to the piece nearest the points the features
|
||||
/// naming it picked. Pieces no later feature names keep the order they came in; so do all of
|
||||
/// them where the picks do not tell the pieces apart.
|
||||
pub(crate) fn order_legacy_lumps(bodies: &mut [crate::ModelBody], before: &crate::ModelState, doc: &crate::Document, f: &crate::Feature) {
|
||||
for members in made_families(bodies, before) {
|
||||
order_legacy_family(bodies, &members, doc, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// The families of [`lump_families`] the feature that left `bodies` after `before` made: a family
|
||||
/// it left as it was keeps its names.
|
||||
fn made_families(bodies: &[crate::ModelBody], before: &crate::ModelState) -> Vec<Vec<usize>> {
|
||||
lump_families(bodies)
|
||||
.into_iter()
|
||||
.filter(|members| members.iter().filter_map(|i| bodies.get(*i)).any(|b| !before.bodies.iter().any(|o| o.body.same(&b.body))))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// What [`order_legacy_lumps`] named the pieces of `bodies` (the state feature `f` left after
|
||||
/// `before`) by: a digest of each piece name with the points the later features of `doc` naming
|
||||
/// it picked, 0 where `f` was saved since format 14 or split nothing. Those features come after
|
||||
/// `f`, so the state is `f`'s only while this is the same: a script replays them after it, and
|
||||
/// an edit may change what they pick or name.
|
||||
pub(crate) fn legacy_picks_digest(bodies: &[crate::ModelBody], before: &crate::ModelState, doc: &crate::Document, f: &crate::Feature) -> u64 {
|
||||
use std::hash::{Hash, Hasher};
|
||||
if !f.shell_piece_order {
|
||||
return 0;
|
||||
}
|
||||
let families = made_families(bodies, before);
|
||||
if families.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
for members in families {
|
||||
let mut names: Vec<String> = members.iter().filter_map(|i| bodies.get(*i).map(|b| b.name.clone())).collect();
|
||||
names.sort();
|
||||
for (name, points) in names.iter().zip(later_picks(doc, f, &names)) {
|
||||
name.hash(&mut h);
|
||||
for p in points {
|
||||
[p.x, p.y, p.z].map(f64::to_bits).hash(&mut h);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 0 means "nothing named so"; a digest that happens to be 0 must not read as that.
|
||||
h.finish().max(1)
|
||||
}
|
||||
|
||||
/// The pieces each body a feature cut in two or more came out as (the bodies whose lineage is
|
||||
/// that one operand), as [`order_lumps`] finds them.
|
||||
fn lump_families(bodies: &[crate::ModelBody]) -> Vec<Vec<usize>> {
|
||||
let mut families: Vec<(*const Operand, Vec<usize>)> = Vec::new();
|
||||
for (i, b) in bodies.iter().enumerate() {
|
||||
let Made::Traced(l) = &b.made else { continue };
|
||||
let [op] = l.operands.as_slice() else { continue };
|
||||
if !matches!(op.as_ref(), Operand::Stage(_)) {
|
||||
continue;
|
||||
}
|
||||
let key = Arc::as_ptr(op);
|
||||
match families.iter_mut().find(|(k, _)| *k == key) {
|
||||
Some((_, members)) => members.push(i),
|
||||
None => families.push((key, vec![i])),
|
||||
}
|
||||
}
|
||||
families.into_iter().map(|(_, m)| m).filter(|m| m.len() > 1).collect()
|
||||
}
|
||||
|
||||
/// Per name of `names`: the points the features after `f` in `doc` that name that body picked
|
||||
/// on it.
|
||||
fn later_picks(doc: &crate::Document, f: &crate::Feature, names: &[String]) -> Vec<Vec<Vec3>> {
|
||||
let later = doc.features.iter().skip_while(|g| g.id != f.id).skip(1);
|
||||
let mut picks: Vec<Vec<Vec3>> = vec![Vec::new(); names.len()];
|
||||
for g in later {
|
||||
let Ok(kind) = serde_json::to_value(&g.kind) else { continue };
|
||||
let Some(k) = kind.get("body").and_then(|b| b.as_str()).and_then(|b| names.iter().position(|n| n == b)) else { continue };
|
||||
for key in ["edges", "faces", "face", "position"] {
|
||||
let point = |v: &serde_json::Value| -> Option<Vec3> {
|
||||
let a = v.as_array().filter(|a| a.len() == 3)?;
|
||||
Some(Vec3::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?, a.get(2)?.as_f64()?))
|
||||
};
|
||||
let found: Vec<Vec3> = match kind.get(key) {
|
||||
Some(v) => {
|
||||
point(v).map(|p| vec![p]).unwrap_or_else(|| v.as_array().map(|a| a.iter().filter_map(point).collect()).unwrap_or_default())
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
if let Some(slot) = picks.get_mut(k) {
|
||||
slot.extend(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
picks
|
||||
}
|
||||
|
||||
fn order_legacy_family(bodies: &mut [crate::ModelBody], members: &[usize], doc: &crate::Document, f: &crate::Feature) {
|
||||
if members.len() > 64 {
|
||||
return;
|
||||
}
|
||||
let names: Vec<String> = members.iter().filter_map(|i| bodies.get(*i).map(|b| b.name.clone())).collect();
|
||||
let picks = later_picks(doc, f, &names);
|
||||
// How far piece `i` is from the points of name `k` (the mesh's nearest triangle).
|
||||
let far = |piece: usize, pts: &[Vec3]| -> f64 {
|
||||
let Some(b) = members.get(piece).and_then(|i| bodies.get(*i)) else { return f64::INFINITY };
|
||||
let m = b.mesh();
|
||||
pts.iter()
|
||||
.map(|p| {
|
||||
m.triangles
|
||||
.iter()
|
||||
.filter_map(|t| m.tri(t))
|
||||
.map(|[a, b, c]| solidcraft_geom::compare::closest_on_triangle(*p, a, b, c).dist(*p))
|
||||
.fold(f64::INFINITY, f64::min)
|
||||
})
|
||||
.fold(0.0, f64::max)
|
||||
};
|
||||
let mut order: Vec<Option<usize>> = vec![None; names.len()];
|
||||
let mut taken = vec![false; members.len()];
|
||||
for (k, pts) in picks.iter().enumerate().filter(|(_, p)| !p.is_empty()) {
|
||||
let best =
|
||||
(0..members.len()).filter(|i| !taken.get(*i).copied().unwrap_or(true)).map(|i| (far(i, pts), i)).min_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let Some((d, i)) = best else { return };
|
||||
// Only a piece the points lie on: a pick off every piece tells nothing.
|
||||
let size = members.get(i).and_then(|m| bodies.get(*m)).map_or(1.0, |b| b.body.size().max(1.0));
|
||||
if d > size * 1e-3 {
|
||||
return;
|
||||
}
|
||||
if let (Some(o), Some(t)) = (order.get_mut(k), taken.get_mut(i)) {
|
||||
*o = Some(i);
|
||||
*t = true;
|
||||
}
|
||||
}
|
||||
let mut free = (0..members.len()).filter(|i| !taken.get(*i).copied().unwrap_or(true));
|
||||
let order: Vec<usize> = order.into_iter().map(|o| o.or_else(|| free.next())).collect::<Option<_>>().unwrap_or_default();
|
||||
if order.len() != names.len() {
|
||||
return;
|
||||
}
|
||||
for (name, piece) in names.into_iter().zip(order) {
|
||||
if let Some(b) = members.get(piece).and_then(|i| bodies.get_mut(*i)) {
|
||||
b.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Name the pieces a body fell into after their faces: of the bodies in `bodies` that are lumps
|
||||
/// of one body (made in the same step, from one stage), the one whose least face name sorts first
|
||||
/// takes the first of their body names, and so on. The kernel's shell order, which named them
|
||||
|
||||
@@ -364,6 +364,10 @@ impl SavedModel {
|
||||
tapered: serde_json::from_value(field("tapered")?).map_err(invalid)?,
|
||||
// A join's notice is for the evaluation that made it, not kept with the cache.
|
||||
kept_separate: BTreeMap::new(),
|
||||
// Not kept either: a saved model's states carry no lineage, so the check of a
|
||||
// legacy split's picks (`lineage::legacy_picks_digest`) finds no pieces in them
|
||||
// and takes them as the design's evaluation they were saved as.
|
||||
legacy_picks: BTreeMap::new(),
|
||||
};
|
||||
held = held.saturating_add(st.bodies.len()).saturating_add(st.sketches.len()).saturating_add(st.sheets.len());
|
||||
if held > MAX_HELD {
|
||||
|
||||
@@ -170,3 +170,12 @@ The #303 draft used `solvecraft-container/1`. `crates/io/src/design.rs` accepts
|
||||
marker alongside `solidcraft-container/1`, and writes only the new marker. Optional
|
||||
old previews are discarded; required document/blob validation is unchanged. The
|
||||
`draft_container_marker_remains_readable` regression covers this shim.
|
||||
|
||||
## Split pieces in designs saved before format 14
|
||||
|
||||
Builds before format 14 named the pieces a cut splits a body into in the old kernel's shell
|
||||
order, and the design's later features name the piece they work on by that name (`Body1`,
|
||||
`Body1 (1)`). SMLib orders pieces otherwise. Such a feature (`shell_piece_order` in the file)
|
||||
names its pieces so its later features find them: each name goes to the piece the points those
|
||||
features picked lie on. A name no later feature uses keeps SMLib's order. Designs saved since
|
||||
format 14 name pieces after their faces and are not affected.
|
||||
|
||||
Reference in New Issue
Block a user