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() {