diff --git a/crates/engine/src/cmd/browser.rs b/crates/engine/src/cmd/browser.rs index b27c8718..a022baf9 100644 --- a/crates/engine/src/cmd/browser.rs +++ b/crates/engine/src/cmd/browser.rs @@ -453,18 +453,24 @@ fn rename_key(d: &mut solidcraft_doc::Document, folder: &str, old: &str, new: &s } } -/// Replace every string equal to `old` with `new`; true when something changed. -fn replace_str(v: &mut Value, old: &str, new: &str, depth: usize) -> bool { +/// The keys whose strings name bodies, alone or in a list (`body`, `targets`…), at any depth of a +/// feature: its own fields and the face, edge and link references inside it. +const BODY_KEYS: [&str; 5] = ["body", "bodies", "target", "targets", "tools"]; + +/// Replace the body name `old` with `new` where a string names a body (the value of one of +/// [`BODY_KEYS`]); other strings equal to it (sketch text, plane and feature names, expressions) +/// stay. True when something changed. +fn replace_body_name(v: &mut Value, old: &str, new: &str, names_body: bool, depth: usize) -> bool { if depth > 64 { return false; } match v { - Value::String(s) if s == old => { + Value::String(s) if names_body && s == old => { *s = new.to_string(); true } - Value::Array(a) => a.iter_mut().fold(false, |acc, x| replace_str(x, old, new, depth + 1) | acc), - Value::Object(o) => o.values_mut().fold(false, |acc, x| replace_str(x, old, new, depth + 1) | acc), + Value::Array(a) => a.iter_mut().fold(false, |acc, x| replace_body_name(x, old, new, names_body, depth + 1) | acc), + Value::Object(o) => o.iter_mut().fold(false, |acc, (k, x)| replace_body_name(x, old, new, BODY_KEYS.contains(&k.as_str()), depth + 1) | acc), _ => false, } } @@ -516,7 +522,7 @@ fn rename_body(s: &mut Session, p: &Value) -> Result { } } let Ok(mut v) = serde_json::to_value(&f.kind) else { continue }; - if replace_str(&mut v, &old, name, 0) { + if replace_body_name(&mut v, &old, name, false, 0) { match serde_json::from_value(v) { Ok(k) => f.kind = k, Err(e) => failed = Some(e.to_string()), @@ -699,6 +705,28 @@ mod tests { assert_eq!(names(&s)[1], b); } + #[test] + fn renaming_a_body_leaves_other_text_with_its_name_alone() { + let mut s = Session::default(); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10})); + let body = names(&s)[0].clone(); + run(&mut s, "solid.move", json!({"bodies": [body.clone()], "translate": [0, 0, 5]})); + // Sketch text that happens to spell the body's name is not a reference to it. + run(&mut s, "sketch.create", json!({"plane": "XY", "name": "Label"})); + run(&mut s, "sketch.text", json!({"text": body.clone(), "at": [30, 30], "height": 5})); + run(&mut s, "sketch.finish", json!({})); + run(&mut s, "body.rename", json!({"body": body.clone(), "name": "Lid"})); + let kind = |name: &str| serde_json::to_value(&s.doc.find_feature(name).unwrap().kind).unwrap(); + let label = kind("Label").to_string(); + assert!(label.contains(&format!("\"{body}\"")) && !label.contains("\"Lid\""), "{label}"); + let moved = s.doc.features.iter().find_map(|f| match &f.kind { + solidcraft_doc::FeatureKind::Move { bodies, .. } => Some(bodies.clone()), + _ => None, + }); + assert_eq!(moved, Some(vec!["Lid".to_string()]), "the move follows the rename"); + assert!(s.model.results.iter().all(|r| r.error.is_none())); + } + #[test] fn redefined_sketch_moves_its_extrude() { let mut s = Session::default();