diff --git a/crates/engine/src/cmd/browser.rs b/crates/engine/src/cmd/browser.rs index acb1f4a9..710f76e7 100644 --- a/crates/engine/src/cmd/browser.rs +++ b/crates/engine/src/cmd/browser.rs @@ -445,13 +445,14 @@ fn rename_body(s: &mut Session, p: &Value) -> Result { if st.body(name).is_some() { return Err(bad(cmd, format!("another body is called `{name}`"))); } - // The body is the k-th new body of its feature; the names before it are kept as they are. + // The body is the k-th new body of its feature; its siblings keep the names they have now + // (unpinned pattern copies would otherwise be renumbered into the freed name, #265). let fid = b.feature; let siblings: Vec = st.bodies.iter().filter(|x| x.feature == fid).map(|x| x.name.clone()).collect(); let k = siblings.iter().position(|n| *n == old).unwrap_or(0); let d = s.doc_mut(); let f = d.feature_mut(fid).ok_or_else(|| bad(cmd, "the body's feature is gone"))?; - while f.body_names.len() <= k { + while f.body_names.len() < siblings.len().max(k + 1) { let i = f.body_names.len(); f.body_names.push(siblings.get(i).cloned().unwrap_or_default()); } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 69b4f930..4cdcc6f6 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2478,6 +2478,20 @@ fn special_files_are_refused_by_every_file_command() { let _ = std::fs::remove_dir_all(&dir); } +/// Renaming one pattern copy leaves the other copies' names alone (#265). +#[test] +fn renaming_a_pattern_copy_keeps_the_other_copies_names() { + let mut s = Session::default(); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "operation": "new_body", "body_name": "X"})); + run(&mut s, "solid.pattern.rectangular", json!({"bodies": ["X"], "dir1": [1, 0, 0], "count1": 3, "spacing1": 20})); + run(&mut s, "body.rename", json!({"body": "X (2)", "name": "Second"})); + let third = run(&mut s, "inspect.measure", json!({"bodies": ["X (3)"]})); + assert_eq!(third["bodies"][0]["bbox"]["min"][0].as_f64().map(f64::round), Some(40.0), "X (3) is still the copy at x = 40: {third}"); + run(&mut s, "body.rename", json!({"body": "X (3)", "name": "Third"})); + let names: Vec = s.model.state().bodies.iter().map(|b| b.name.clone()).collect(); + assert_eq!(names, ["X", "Second", "Third"]); +} + /// Parameters that work are listed in the command's params doc, which is all agents see (#271). #[test] fn params_docs_list_working_parameters() {