diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 9cac5c97..2bb39069 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -1,5 +1,7 @@ //! Undo/redo, the timeline, deletion and selection. +use std::collections::BTreeMap; + use serde_json::{Value, json}; use solidcraft_doc::{Feature, FeatureKind}; @@ -22,7 +24,7 @@ pub static COMMANDS: &[CommandSpec] = &[ .icon("delete") .key("Delete") .params("features: [id or name] (a sketch takes its dependent features with it) → deleted: [ids]; failing?: [{feature, error}] (later features that worked and now fail)"), - CommandSpec::new("timeline.rename", "Rename", rename).params("feature: id|name, name"), + CommandSpec::new("timeline.rename", "Rename", rename).params("feature: id|name, name (references to it follow)"), CommandSpec::new("timeline.rollback", "Move Timeline Marker", rollback).params("position: number of features to keep active (omit = end)"), CommandSpec::new("timeline.roll_to", "Roll History Marker", rollback) .params("position: number of features to keep active (omit = end) | feature: id|name (the marker goes right after it)"), @@ -163,15 +165,25 @@ fn rename(s: &mut Session, p: &Value) -> Result { let id = feature_id(s, p.get("feature"), cmd)?; let name = str_(p, "name").map(str::trim).filter(|n| !n.is_empty() && n.len() <= 128).ok_or_else(|| bad(cmd, "`name` must be 1…128 characters"))?; + let old = s.doc.feature(id).map(|f| f.name.clone()).ok_or_else(|| bad(cmd, "feature"))?; if s.doc.features.iter().any(|f| f.name == name && f.id != id) { return Err(bad(cmd, format!("another feature is called `{name}`"))); } if s.doc.feature(id).is_some_and(|f| matches!(f.kind, FeatureKind::ConstructionPlane { .. })) { super::features::check_plane_name(name, cmd)?; } - if let Some(f) = s.doc_mut().feature_mut(id) { + if name == old { + return Ok(json!({"feature": id, "name": name})); + } + let names = BTreeMap::from([(old.clone(), name.to_string())]); + let sketches = BTreeMap::new(); + let doc = s.doc_mut(); + if let Some(f) = doc.feature_mut(id) { f.name = name.to_string(); } + for f in doc.features.iter_mut() { + f.kind.remap_refs(&sketches, &names); + } Ok(json!({"feature": id, "name": name})) } diff --git a/crates/engine/src/cmd/patterns_tests.rs b/crates/engine/src/cmd/patterns_tests.rs index c94ffbd7..237e7669 100644 --- a/crates/engine/src/cmd/patterns_tests.rs +++ b/crates/engine/src/cmd/patterns_tests.rs @@ -208,6 +208,25 @@ fn patterns_of_a_blind_hole_drill_every_copy() { assert!((c - 2.0 * one).abs() < 1e-3 * one, "{c} vs 2 × {one}"); } +/// Feature patterns and mirrors reference features by name; a rename follows (#401). +#[test] +fn renaming_a_source_feature_follows_pattern_and_mirror_references() { + let mut s = plate_with_hole(); + run(&mut s, "construct.plane.offset", json!({"base": "YZ", "offset": 0, "name": "Mirror plane"})); + run(&mut s, "solid.pattern.rectangular", json!({"features": ["Hole1"], "dir1": [1, 0, 0], "count1": 2, "spacing1": 20, "name": "Copies"})); + run(&mut s, "solid.mirror", json!({"features": ["Hole1"], "plane": "Mirror plane", "name": "Mirrored"})); + let renamed = run(&mut s, "timeline.rename", json!({"feature": "Hole1", "name": "Single hole"})); + assert_eq!(renamed["name"], "Single hole", "{renamed}"); + run(&mut s, "timeline.rename", json!({"feature": "Mirror plane", "name": "Renamed plane"})); + for feature in ["Copies", "Mirrored"] { + let inspected = run(&mut s, "timeline.inspect", json!({"feature": feature})); + assert!(inspected["error"].is_null(), "{feature}: {inspected}"); + assert_eq!(inspected["definition"]["features"], json!(["Single hole"]), "{inspected}"); + } + let inspected = run(&mut s, "timeline.inspect", json!({"feature": "Mirrored"})); + assert_eq!(inspected["definition"]["plane"]["name"], "Renamed plane", "{inspected}"); +} + /// A plate with a `count1` × `count2` feature pattern of one through-hole cut: its volume and how /// long the pattern took. It runs on its own thread so a hang fails the test instead of CI. fn hole_pattern(count1: usize, count2: usize) -> (f64, std::time::Duration) {