Files
cadcraft/crates/engine/tests/rename_cmdline.rs
Valentin KleinandClaude Opus 5.5 9524f9a5c2 -RENAME prompts; RENAME renames blocks (fixes #264) (#266)
Typed RENAME / -RENAME (and REN) asks for the object type
[Block/Dimstyle/LAyer/LType/Material/multileadeRstyle/Plotstyle/
textStyle/Tablestyle/Ucs/VIew/VPort], the old name and the new name,
then runs the rename command body, so the checks and the single undo
step match the JSON form. An unknown old name or a refused new name
re-prompts. Material, Plotstyle and VPort read both names and report
"not available yet".

The JSON form now renames blocks, as its params doc promised. Every
INSERT follows the new name, in model space, layouts and nested block
definitions, including inserts that carry attributes. INSNAME and
dimension-style arrowheads follow too. Duplicate names (any case),
invalid names and anonymous blocks are refused. Multileader styles,
table styles, named views and UCSs can be renamed as well. A layer
rename no longer records two undo steps.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:12:03 -04:00

89 lines
5.0 KiB
Rust

//! RENAME: blocks in the JSON form, and -RENAME's type / old name / new name prompts.
use cadcraft_engine::Session;
use cadcraft_engine::doc::{Drawing, EntityKind};
use serde_json::json;
/// Names of the blocks every INSERT (model, layouts and block definitions) refers to.
fn insert_targets(d: &Drawing) -> Vec<String> {
let ents = d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())).chain(d.blocks.values().flat_map(|b| b.entities.iter()));
let mut v: Vec<String> = ents.filter_map(|e| if let EntityKind::Insert(i) = &e.kind { Some(i.block.clone()) } else { None }).collect();
v.sort();
v
}
/// Valve (with an attribute) inserted in model space, in Layout1 and nested inside Assy.
fn drawing() -> Session {
let mut s = Session::new();
s.execute("circle", &json!({ "center": [0, 0], "radius": 1 })).unwrap();
s.execute("attdef", &json!({ "tag": "TAG", "at": [0, 2] })).unwrap();
s.execute("selectall", &json!({})).unwrap();
s.execute("block", &json!({ "name": "Valve", "base": [0, 0], "keep": "delete" })).unwrap();
let h =
s.execute("insert", &json!({ "name": "Valve", "at": [0, 0], "attribs": { "TAG": "V-1" } })).unwrap()["handle"].as_str().unwrap().to_string();
s.execute("block", &json!({ "name": "Assy", "base": [0, 0], "handles": [h], "keep": "convert" })).unwrap();
s.execute("insert", &json!({ "name": "Valve", "at": [5, 0], "attribs": { "TAG": "V-2" } })).unwrap();
s.execute("layout.set", &json!({ "name": "Layout1" })).unwrap();
s.execute("insert", &json!({ "name": "Valve", "at": [1, 1] })).unwrap();
s.execute("layout.set", &json!({ "name": "Model" })).unwrap();
s
}
#[test]
fn json_block_rename_repoints_every_insert() {
let mut s = drawing();
assert_eq!(insert_targets(s.doc().unwrap()), ["Assy", "Valve", "Valve", "Valve"]);
let undo = s.state().unwrap().undo.len();
s.execute("rename", &json!({ "table": "block", "from": "valve", "to": "Pump" })).unwrap();
assert_eq!(s.state().unwrap().undo.len(), undo + 1);
let d = s.doc().unwrap();
assert!(d.block("Valve").is_none());
assert_eq!(d.block("Pump").unwrap().name, "Pump");
assert_eq!(insert_targets(d), ["Assy", "Pump", "Pump", "Pump"]);
// The attribute values stay on the renamed inserts.
assert!(d.model.iter().any(|e| matches!(&e.kind, EntityKind::Insert(i) if i.block == "Pump" && i.attribs.iter().any(|a| a.text.value == "V-2"))));
// Duplicates (any case), invalid names and unknown blocks are refused.
assert!(s.execute("rename", &json!({ "table": "block", "from": "Pump", "to": "ASSY" })).is_err());
assert!(s.execute("rename", &json!({ "table": "block", "from": "Pump", "to": "a*b" })).is_err());
assert!(s.execute("rename", &json!({ "table": "block", "from": "Nope", "to": "X" })).is_err());
assert_eq!(insert_targets(s.doc().unwrap()), ["Assy", "Pump", "Pump", "Pump"]);
// Multileader styles: the current-style variable follows; Standard stays.
s.execute("mleaderstyle", &json!({ "name": "Callout", "current": true })).unwrap();
s.execute("rename", &json!({ "table": "mleaderstyle", "from": "Callout", "to": "Notes" })).unwrap();
assert_eq!(s.doc().unwrap().header.str("CMLEADERSTYLE", ""), "Notes");
assert!(s.execute("rename", &json!({ "table": "mleaderstyle", "from": "Standard", "to": "X" })).is_err());
// A layer rename is one undo step too.
s.execute("layer.new", &json!({ "name": "Old" })).unwrap();
let undo = s.state().unwrap().undo.len();
s.execute("rename", &json!({ "table": "layer", "from": "Old", "to": "New" })).unwrap();
assert_eq!(s.state().unwrap().undo.len(), undo + 1);
}
#[test]
fn rename_prompts_type_old_and_new_name() {
let mut s = drawing();
s.cmdline("-RENAME").unwrap();
assert!(
s.prompt_text().contains("[Block/Dimstyle/LAyer/LType/Material/multileadeRstyle/Plotstyle/textStyle/Tablestyle/Ucs/VIew/VPort]"),
"{}",
s.prompt_text()
);
s.cancel();
s.execute("layer.new", &json!({ "name": "Old" })).unwrap();
s.script("-RENAME B Valve Pump\n-RENAME LA Old New\nLINE 0,0 1,0\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
let d = s.doc().unwrap();
assert!(d.block("Pump").is_some() && d.layer("New").is_some() && d.layer("Old").is_none(), "{:?}", s.log);
assert_eq!(insert_targets(d), ["Assy", "Pump", "Pump", "Pump"]);
assert_eq!(d.model.iter().filter(|e| matches!(e.kind, EntityKind::Line(_))).count(), 1);
// An unknown old name re-prompts; a type CADCraft lacks reads both names and reports.
s.script("-RENAME LA Missing\nNew\nNewer\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert!(s.doc().unwrap().layer("Newer").is_some());
let n = s.log.len();
s.script("-RENAME VP a b\nLINE 0,0 2,0\n\n").unwrap();
assert!(s.running.is_none(), "{}", s.prompt_text());
assert!(s.log[n..].iter().any(|l| l.contains("not available yet")), "{:?}", &s.log[n..]);
assert_eq!(s.doc().unwrap().model.iter().filter(|e| matches!(e.kind, EntityKind::Line(_))).count(), 2);
}