From f93577ad708b642f2ece914cbf33d888fc171fb2 Mon Sep 17 00:00:00 2001 From: Mqrius Date: Tue, 13 Oct 2026 06:25:28 +0200 Subject: [PATCH] fix(doc): pieces of a body cut apart again get names of their own (#512) When a body that was already cut into pieces is cut again, the new pieces are numbered past the names already taken ("Bar (2)"), instead of reusing "Bar (1)". Co-Authored-By: Claude Opus 5.5 --- crates/doc/src/eval.rs | 11 +++++++++-- crates/engine/src/tests.rs | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/doc/src/eval.rs b/crates/doc/src/eval.rs index e55a8cdb..09d5e585 100644 --- a/crates/doc/src/eval.rs +++ b/crates/doc/src/eval.rs @@ -1323,7 +1323,9 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec, op: Ope }); let live: std::collections::BTreeSet<&str> = state.bodies.iter().map(|b| b.name.as_str()).collect(); state.threads.retain(|t| live.contains(t.body.as_str())); - // A cut can leave a body in pieces: each piece becomes its own body ("Bar", "Bar (1)"). + // A cut can leave a body in pieces: each piece becomes its own body ("Bar", "Bar (1)"), + // numbered on past names already taken (a body cut in pieces before). + let mut taken: std::collections::BTreeSet = state.bodies.iter().map(|b| b.name.clone()).collect(); let mut split: Vec = Vec::new(); for mb in std::mem::take(&mut state.bodies) { if !affected.contains(&mb.name) || state.sheets.iter().any(|s| s.body == mb.name) { @@ -1339,7 +1341,12 @@ fn apply_traced(state: &mut ModelState, f: &Feature, tools: Vec, op: Ope // (so pieces of one face in different bodies are told apart). let lineages = crate::lineage::Lineage::lumps(&mb.body, mb.lineage().as_deref(), &pieces); for (k, (p, lineage)) in pieces.into_iter().zip(lineages).enumerate() { - let name = if k == 0 { mb.name.clone() } else { format!("{} ({k})", mb.name) }; + let name = if k == 0 { + mb.name.clone() + } else { + (k..=k + taken.len()).map(|j| format!("{} ({j})", mb.name)).find(|n| !taken.contains(n)).unwrap_or_default() + }; + taken.insert(name.clone()); split.push(ModelBody::new(name, p, mb.feature).with_lineage(lineage)); } } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index e08f54fb..0222822a 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -96,6 +96,20 @@ fn join_extrude_from_a_chamfered_top_face() { } } +#[test] +fn a_body_cut_in_pieces_twice_gets_pieces_of_their_own_names() { + let mut s = Session::default(); + run(&mut s, "solid.box", json!({"length": 40, "width": 5, "height": 5, "body_name": "Bar"})); + for x in [10, 25] { + run(&mut s, "solid.box", json!({"corner": [x, -1, -1], "length": 2, "width": 7, "height": 7, "body_name": "Gap"})); + run(&mut s, "solid.combine", json!({"target": "Bar", "tools": ["Gap"], "operation": "cut"})); + } + let names: Vec = s.model.state().bodies.iter().map(|b| b.name.clone()).collect(); + let unique: std::collections::BTreeSet<&String> = names.iter().collect(); + assert_eq!(unique.len(), 3, "{names:?}"); + assert!(names.contains(&"Bar (2)".to_string()), "{names:?}"); +} + /// A join that bridges two bodies makes them one body, not an overlap of the first and the second. #[test] fn join_bridging_two_bodies_merges_them() {