fix: follow feature renames in references (#486)

Fixes #401
This commit is contained in:
onenewcode authored and GitHub committed 2026-10-12 22:39:13 -04:00
1 parent 91de39d3fe
commit 00baf1d883
2 files changed
+33 -2

No files matched your search

+14 -2
View File
@@ -1,5 +1,7 @@
//! Undo/redo, the timeline, deletion and selection. //! Undo/redo, the timeline, deletion and selection.
use std::collections::BTreeMap;
use serde_json::{Value, json}; use serde_json::{Value, json};
use solidcraft_doc::{Feature, FeatureKind}; use solidcraft_doc::{Feature, FeatureKind};
@@ -22,7 +24,7 @@ pub static COMMANDS: &[CommandSpec] = &[
.icon("delete") .icon("delete")
.key("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)"), .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.rollback", "Move Timeline Marker", rollback).params("position: number of features to keep active (omit = end)"),
CommandSpec::new("timeline.roll_to", "Roll History Marker", rollback) 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)"), .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<Value> {
let id = feature_id(s, p.get("feature"), cmd)?; let id = feature_id(s, p.get("feature"), cmd)?;
let name = 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"))?; 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) { if s.doc.features.iter().any(|f| f.name == name && f.id != id) {
return Err(bad(cmd, format!("another feature is called `{name}`"))); return Err(bad(cmd, format!("another feature is called `{name}`")));
} }
if s.doc.feature(id).is_some_and(|f| matches!(f.kind, FeatureKind::ConstructionPlane { .. })) { if s.doc.feature(id).is_some_and(|f| matches!(f.kind, FeatureKind::ConstructionPlane { .. })) {
super::features::check_plane_name(name, cmd)?; 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(); f.name = name.to_string();
} }
for f in doc.features.iter_mut() {
f.kind.remap_refs(&sketches, &names);
}
Ok(json!({"feature": id, "name": name})) Ok(json!({"feature": id, "name": name}))
} }
+19
View File
@@ -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}"); 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 /// 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. /// 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) { fn hole_pattern(count1: usize, count2: usize) -> (f64, std::time::Duration) {