From cb8aa38cc9db3effa47932a1dbd11e9560c86513 Mon Sep 17 00:00:00 2001 From: Jonas Wrede Date: Mon, 12 Oct 2026 17:24:33 +0200 Subject: [PATCH] test: failing regression test for #266: Multi-profile solid.extrude with body_name names only the first body (#281) * test: reproduce #266 (Multi-profile solid.extrude with body_name names only the first body) * fix: name every body of a multi-body feature after body_name (#266) A new-body feature that makes more bodies than it was given names now stores Ribs (2), Ribs (3)... for the rest instead of falling back to generic BodyN names. Command-time only: saved designs keep their names. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT * fix: keep body_name list for the multi-body naming step (#266) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: bherbruck Co-authored-by: Claude Opus 5.5 --- crates/engine/src/cmd/features.rs | 25 ++++++++++++++++++++++++- crates/engine/src/tests.rs | 15 +++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/features.rs b/crates/engine/src/cmd/features.rs index b5117f69..64b6ff50 100644 --- a/crates/engine/src/cmd/features.rs +++ b/crates/engine/src/cmd/features.rs @@ -443,10 +443,33 @@ fn add_feature_in(s: &mut Session, p: &Value, kind: FeatureKind, world: bool) -> if !names.is_empty() && let Some(f) = s.doc_mut().feature_mut(id) { - f.body_names = names; + f.body_names = names.clone(); } s.active_sketch = None; s.refresh(); + // A feature that makes more bodies than it was given names (a multi-profile extrude with one + // `body_name`) names the rest after the first: `Ribs`, `Ribs (2)`, `Ribs (3)`… (#266). The + // names are stored on the feature, so recomputes and saved designs keep them. + if let Some(base) = names.first().map(|n| n.trim().to_string()).filter(|n| !n.is_empty()) { + let st = s.model.state(); + let made = st.bodies.iter().filter(|b| b.feature == id).count(); + if made > names.len() { + let mut taken: std::collections::BTreeSet = st.bodies.iter().map(|b| b.name.clone()).collect(); + taken.extend(names.iter().cloned()); + let mut i = 1usize; + while names.len() < made { + i += 1; + let n = format!("{base} ({i})"); + if taken.insert(n.clone()) { + names.push(n); + } + } + if let Some(f) = s.doc_mut().feature_mut(id) { + f.body_names = names; + } + s.refresh(); + } + } let r = s.model.result(id).cloned(); if let Some(e) = r.as_ref().and_then(|r| r.error.clone()) { return Err(EngineError::Other(e)); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index fe121b45..ceafd9bb 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2489,6 +2489,21 @@ fn special_files_are_refused_by_every_file_command() { let _ = std::fs::remove_dir_all(&dir); } +/// Every body a multi-profile `new` extrude creates carries its `body_name`, not a generic BodyN (#266). +#[test] +fn multi_profile_extrude_names_every_body_after_body_name() { + let mut s = Session::default(); + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "R"})); + for x in [0, 10, 20] { + run(&mut s, "sketch.rectangle.two_point", json!({"p0": [x, 0], "p1": [x + 5, 30]})); + } + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "solid.extrude", json!({"distance": "10", "operation": "new_body", "body_name": "Ribs"})); + let names: Vec = s.model.state().bodies.iter().map(|b| b.name.clone()).collect(); + assert!(names.iter().all(|n| n.starts_with("Ribs")), "{names:?}"); + assert!(rel(volume(&mut s), 4500.0) < 1e-6); +} + /// Renaming one pattern copy leaves the other copies' names alone (#265). #[test] fn renaming_a_pattern_copy_keeps_the_other_copies_names() {