Compare commits

...

1 Commits

Author SHA1 Message Date
bherbruck c0236187d1 Renaming a body that later features change evaluates nothing (#2467)
Model::rename_body now keeps the results when later features change the
body (a pattern, a chamfer, a join). It still evaluates when a body of
that name comes and goes or isn't the same body throughout, when a later
body's name is worked out from it (X (2)), or when a message quotes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:36:00 -04:00
2 changed files with 77 additions and 6 deletions
+19 -6
View File
@@ -1532,23 +1532,36 @@ impl Model {
/// Follow a rename of body `old` to `new` without evaluating anything (#3044): `doc` is the
/// design after the rename, which changed nothing but names, so every result keeps its
/// geometry and meshes and only the body's name (and the names worked out from it) changes.
/// Later features that change the body (a pattern, a chamfer, a join) keep their results
/// too (#2467): a feature's geometry doesn't depend on its bodies' names.
/// False, leaving the model as it was, when the rename can't be followed that way and the
/// design must be evaluated: the model isn't the design's, the body is changed by a later
/// feature, or is sheet metal, threaded, or named in a later feature's face references.
/// design must be evaluated: the model isn't the design's, a body of that name comes and goes
/// or isn't the same body throughout, a later body's name is worked out from it (`X (2)`), a
/// message quotes it, or it is sheet metal, threaded, or named in a later feature's face
/// references.
pub fn rename_body(&mut self, doc: &Document, old: &str, new: &str) -> bool {
if self.results.len() != doc.features.len() || self.results.iter().zip(&doc.features).any(|(r, f)| r.id != f.id || r.stale) {
return false;
}
let Some(at) = self.results.iter().position(|r| r.state.body(old).is_some()) else { return false };
let Some(made) = self.results.get(at).and_then(|r| r.state.body(old)).map(|b| Arc::as_ptr(&b.mesh)) else { return false };
// Untouched from the feature that made it to the end, and nothing else holds its name.
let untouched = self.results.iter().skip(at).all(|r| r.state.body(old).is_some_and(|b| Arc::as_ptr(&b.mesh) == made));
let Some(feature) = self.results.get(at).and_then(|r| r.state.body(old)).map(|b| b.feature) else { return false };
// One body from the feature that made it until it is gone (joined into another), and no
// other body's name comes from its name.
let held: Vec<bool> = self.results.iter().skip(at).map(|r| r.state.body(old).is_some()).collect();
let one_run = held.iter().skip_while(|h| **h).all(|h| !*h);
let derived = format!("{old} (");
let same_body = self
.results
.iter()
.skip(at)
.all(|r| r.state.bodies.iter().all(|b| if b.name == old { b.feature == feature } else { !b.name.starts_with(&derived) }));
let quoted = self.results.iter().skip(at).any(|r| [&r.error, &r.warning].into_iter().flatten().any(|m| m.contains(old)));
let named_elsewhere = self
.results
.iter()
.any(|r| r.state.body(new).is_some() || r.state.sheets.iter().any(|x| x.body == old) || r.state.threads.iter().any(|t| t.body == old));
let faces_named = doc.features.iter().skip(at + 1).any(|f| f.face_names.iter().any(|n| n.contains(old)));
if !untouched || named_elsewhere || faces_named {
if !one_run || !same_body || quoted || named_elsewhere || faces_named {
return false;
}
// States shared between results stay shared.
+58
View File
@@ -3851,6 +3851,64 @@ fn renaming_an_imported_assembly_body_reads_nothing_again() {
let _ = std::fs::remove_dir_all(&dir);
}
/// #2467: renaming a body that later features change (a hole patterned over it, then a chamfer)
/// evaluates nothing, and leaves the bodies, volumes and face names evaluating the renamed
/// design gives; undo and redo do the same.
#[test]
fn renaming_a_body_later_features_change_evaluates_nothing() {
let mut s = Session::default();
run(&mut s, "sketch.create", json!({"plane": "XY"}));
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [80, 50]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "solid.extrude", json!({"distance": 10}));
run(
&mut s,
"solid.hole",
json!({"position": [15, 12, 10], "diameter": 5, "type": "counterbore", "cb_diameter": 9, "cb_depth": 3, "name": "Hole1"}),
);
run(
&mut s,
"solid.pattern.rectangular",
json!({"features": ["Hole1"], "dir1": [1, 0, 0], "count1": 3, "spacing1": 25, "dir2": [0, 1, 0], "count2": 2, "spacing2": 25}),
);
run(&mut s, "solid.chamfer", json!({"edges": [[40, 0, 10], [80, 25, 10]], "distance": 1}));
let looks = |st: &solidcraft_doc::ModelState| {
st.bodies.iter().map(|b| (b.name.clone(), (*solidcraft_doc::naming::face_names(b)).clone())).collect::<Vec<_>>()
};
let before = looks(&s.model.state());
let v = body_volume(&s.model.state());
let old = before.first().map(|b| b.0.clone()).unwrap();
s.evaluations = 0;
run(&mut s, "body.rename", json!({"body": old, "name": "Plate"}));
assert_eq!(s.evaluations, 0, "a rename evaluates nothing");
let after = looks(&s.model.state());
assert_eq!(after.first().map(|b| b.0.as_str()), Some("Plate"));
let mut fresh = Session::new((*s.doc).clone());
fresh.refresh();
assert_eq!(looks(&fresh.model.state()), after, "the bodies and face names evaluating the renamed design gives");
assert!(rel(body_volume(&fresh.model.state()), v) < 1e-9);
run(&mut s, "edit.undo", json!({}));
assert_eq!(looks(&s.model.state()), before);
run(&mut s, "edit.redo", json!({}));
assert_eq!(looks(&s.model.state()), after);
assert_eq!(s.evaluations, 0, "nor do its undo and redo");
}
/// A body whose name later bodies' names are worked out from (`X` patterned into `X (2)`) is still
/// renamed by evaluating the design, so the copies follow it as they would on opening it.
#[test]
fn renaming_a_body_whose_copies_take_its_name_evaluates_the_design() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "body_name": "X"}));
run(&mut s, "solid.pattern.rectangular", json!({"bodies": ["X"], "dir1": [1, 0, 0], "count1": 3, "spacing1": 20}));
let names = |s: &Session| s.model.state().bodies.iter().map(|b| b.name.clone()).collect::<Vec<_>>();
assert!(names(&s).iter().any(|n| n.starts_with("X (")), "{:?}", names(&s));
run(&mut s, "body.rename", json!({"body": "X", "name": "First"}));
let mut fresh = Session::new((*s.doc).clone());
fresh.refresh();
assert_eq!(names(&fresh), names(&s));
}
/// #3044: on a design with thousands of bodies the cheap commands stay cheap: showing, hiding,
/// selecting, colouring and renaming a body or a feature, and undoing them, recompute no feature
/// (renames don't even look over the timeline) and leave every body's display mesh as it was, so