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>
This commit is contained in:
parent
824ae58d91
commit
9524f9a5c2
|
|
@ -1,4 +1,6 @@
|
|||
//! Properties: per-object property edits, current properties, MATCHPROP, linetypes, units.
|
||||
//! Properties: per-object property edits, current properties, MATCHPROP, linetypes, units, RENAME.
|
||||
|
||||
mod rename;
|
||||
|
||||
use cadcraft_color::Color;
|
||||
use cadcraft_doc::{EntityKind, Lineweight, Transparency};
|
||||
|
|
@ -47,7 +49,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
.alias(&["mls"])
|
||||
.params("{name, arrowSize?, textHeight?, landingGap?, dogleg?, textStyle?, current?} → styles"),
|
||||
CommandSpec::new("ddptype", "Point Style...", run_ptype).menu(&["Format", "Point Style..."]).params("{pdmode, pdsize}"),
|
||||
CommandSpec::new("rename", "Rename...", run_rename).menu(&["Format", "Rename..."]).params("{table: layer|linetype|style|dimstyle|block, from, to}"),
|
||||
CommandSpec::new("rename", "Rename...", run_rename)
|
||||
.menu(&["Format", "Rename..."])
|
||||
.alias(&["ren", "-rename"])
|
||||
.params("{table: layer|linetype|style|dimstyle|block|mleaderstyle|tablestyle|view|ucs, from, to}")
|
||||
.interactive(|_| Ok(Box::new(rename::RenameM::default()))),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -859,15 +865,20 @@ fn run_rename(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
let to = str_param(p, "to").ok_or_else(|| bad("rename", "`to` is required"))?.to_string();
|
||||
match table.as_str() {
|
||||
"layer" => {
|
||||
s.execute("layer.set", &json!({ "name": from, "newName": to }))?;
|
||||
// The command body, not `execute`: RENAME records the one undo step.
|
||||
let set = find_command("layer.set").ok_or_else(|| bad("rename", "no layer.set command"))?;
|
||||
(set.run)(s, &json!({ "name": from, "newName": to }))?;
|
||||
}
|
||||
"style" => {
|
||||
"block" => rename::block(s, &from, &to)?,
|
||||
"mleaderstyle" | "multileaderstyle" => rename::simple(s, "mleaderstyle", &from, &to)?,
|
||||
"tablestyle" | "view" | "ucs" => rename::simple(s, &table, &from, &to)?,
|
||||
"style" | "textstyle" => {
|
||||
run_style_rename(s, &json!({ "from": from, "to": to }))?;
|
||||
}
|
||||
"dimstyle" => {
|
||||
run_dimstyle_rename(s, &json!({ "from": from, "to": to }))?;
|
||||
}
|
||||
"linetype" => {
|
||||
"linetype" | "ltype" => {
|
||||
let d = s.doc_mut()?;
|
||||
if ["ByBlock", "ByLayer", "Continuous"].iter().any(|n| from.eq_ignore_ascii_case(n)) {
|
||||
return Err(bad("rename", format!("the {from} linetype cannot be renamed")));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,247 @@
|
|||
//! RENAME for blocks and the remaining named tables, and -RENAME's prompts.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use cadcraft_doc::{Drawing, EntityKind, EntityStore, Handle};
|
||||
use serde_json::json;
|
||||
|
||||
use super::super::bad;
|
||||
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
|
||||
|
||||
fn valid_name(n: &str) -> bool {
|
||||
!n.trim().is_empty() && n.len() <= 255 && !n.starts_with('*') && !n.chars().any(|c| "<>/\\\":;?*|,=`".contains(c))
|
||||
}
|
||||
|
||||
/// Handles in `store` whose entity `uses` the old name.
|
||||
fn users(store: &EntityStore, uses: &dyn Fn(&EntityKind) -> bool) -> Vec<Handle> {
|
||||
store.iter().filter(|e| uses(&e.kind)).map(|e| e.handle).collect()
|
||||
}
|
||||
|
||||
/// Apply `fix` to every entity that `uses` the old name: model space, layouts and block definitions.
|
||||
fn repoint(d: &mut Drawing, uses: &dyn Fn(&EntityKind) -> bool, fix: &dyn Fn(&mut EntityKind)) -> Result<()> {
|
||||
let mut hs = users(&d.model, uses);
|
||||
hs.extend(d.layouts.iter().flat_map(|l| users(&l.entities, uses)));
|
||||
for h in hs {
|
||||
d.modify_entity(h, |e| fix(&mut e.kind))?;
|
||||
}
|
||||
for block in d.blocks.values_mut() {
|
||||
let hs = users(&block.entities, uses);
|
||||
if !hs.is_empty() {
|
||||
let b = Arc::make_mut(block);
|
||||
for h in hs {
|
||||
b.entities.modify(h, |e| fix(&mut e.kind));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rename a block definition; inserts everywhere (nested ones and those with attributes
|
||||
/// included), INSNAME and dimension-style arrowheads follow.
|
||||
pub(super) fn block(s: &mut Session, from: &str, to: &str) -> Result<()> {
|
||||
let to = to.trim().to_string();
|
||||
if !valid_name(&to) {
|
||||
return Err(bad("rename", "invalid block name"));
|
||||
}
|
||||
let d = s.doc_mut()?;
|
||||
let key = d.blocks.keys().find(|k| k.eq_ignore_ascii_case(from)).cloned().ok_or_else(|| bad("rename", "no such block"))?;
|
||||
if key.starts_with('*') || d.blocks.get(&key).is_some_and(|b| b.anonymous) {
|
||||
return Err(bad("rename", "an anonymous block cannot be renamed"));
|
||||
}
|
||||
if !to.eq_ignore_ascii_case(&key) && d.block(&to).is_some() {
|
||||
return Err(bad("rename", format!("a block `{to}` already exists")));
|
||||
}
|
||||
let mut b = d.blocks.remove(&key).ok_or_else(|| bad("rename", "no such block"))?;
|
||||
Arc::make_mut(&mut b).name = to.clone();
|
||||
d.blocks.insert(to.clone(), b);
|
||||
let uses = |k: &EntityKind| matches!(k, EntityKind::Insert(i) if i.block.eq_ignore_ascii_case(&key));
|
||||
let fix = |k: &mut EntityKind| {
|
||||
if let EntityKind::Insert(i) = k {
|
||||
i.block = to.clone();
|
||||
}
|
||||
};
|
||||
repoint(d, &uses, &fix)?;
|
||||
if d.header.str("INSNAME", "").eq_ignore_ascii_case(&key) {
|
||||
d.header.set_str("INSNAME", &to);
|
||||
}
|
||||
for ds in &mut d.dim_styles {
|
||||
for a in [&mut ds.arrow_block, &mut ds.arrow_block1, &mut ds.arrow_block2] {
|
||||
if a.eq_ignore_ascii_case(&key) {
|
||||
*a = to.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rename a multileader style, a table style, a named view or a named UCS.
|
||||
pub(super) fn simple(s: &mut Session, table: &str, from: &str, to: &str) -> Result<()> {
|
||||
let to = to.trim().to_string();
|
||||
if !valid_name(&to) {
|
||||
return Err(bad("rename", "invalid name"));
|
||||
}
|
||||
if matches!(table, "mleaderstyle" | "tablestyle") && from.eq_ignore_ascii_case("Standard") {
|
||||
return Err(bad("rename", "the Standard style cannot be renamed"));
|
||||
}
|
||||
let d = s.doc_mut()?;
|
||||
let mut names: Vec<&mut String> = match table {
|
||||
"mleaderstyle" => d.mleader_styles.iter_mut().map(|t| &mut t.name).collect(),
|
||||
"tablestyle" => d.table_styles.iter_mut().map(|t| &mut t.name).collect(),
|
||||
"view" => d.views.iter_mut().map(|t| &mut t.name).collect(),
|
||||
"ucs" => d.ucss.iter_mut().map(|t| &mut t.name).collect(),
|
||||
_ => return Err(bad("rename", "unsupported table")),
|
||||
};
|
||||
if !to.eq_ignore_ascii_case(from) && names.iter().any(|n| n.eq_ignore_ascii_case(&to)) {
|
||||
return Err(bad("rename", format!("`{to}` already exists")));
|
||||
}
|
||||
let n = names.iter_mut().find(|n| n.eq_ignore_ascii_case(from)).ok_or_else(|| bad("rename", format!("no {table} `{from}`")))?;
|
||||
let old = std::mem::replace(&mut **n, to.clone());
|
||||
let current = match table {
|
||||
"mleaderstyle" => "CMLEADERSTYLE",
|
||||
"tablestyle" => "CTABLESTYLE",
|
||||
_ => return Ok(()),
|
||||
};
|
||||
if d.header.str(current, "Standard").eq_ignore_ascii_case(&old) {
|
||||
d.header.set_str(current, &to);
|
||||
}
|
||||
let uses = |k: &EntityKind| match k {
|
||||
EntityKind::MLeader(m) => table == "mleaderstyle" && m.style.eq_ignore_ascii_case(&old),
|
||||
EntityKind::Table(t) => table == "tablestyle" && t.style.eq_ignore_ascii_case(&old),
|
||||
_ => false,
|
||||
};
|
||||
let fix = |k: &mut EntityKind| match k {
|
||||
EntityKind::MLeader(m) => m.style = to.clone(),
|
||||
EntityKind::Table(t) => t.style = to.clone(),
|
||||
_ => {}
|
||||
};
|
||||
repoint(d, &uses, &fix)
|
||||
}
|
||||
|
||||
// ---------------- -RENAME prompts ----------------
|
||||
|
||||
/// Prompt keyword, `rename` table (None: not available yet), noun.
|
||||
const TYPES: &[(&str, Option<&str>, &str)] = &[
|
||||
("Block", Some("block"), "block"),
|
||||
("Dimstyle", Some("dimstyle"), "dimension style"),
|
||||
("LAyer", Some("layer"), "layer"),
|
||||
("LType", Some("linetype"), "linetype"),
|
||||
("Material", None, "material"),
|
||||
("multileadeRstyle", Some("mleaderstyle"), "multileader style"),
|
||||
("Plotstyle", None, "plot style"),
|
||||
("textStyle", Some("style"), "text style"),
|
||||
("Tablestyle", Some("tablestyle"), "table style"),
|
||||
("Ucs", Some("ucs"), "UCS"),
|
||||
("VIew", Some("view"), "view"),
|
||||
("VPort", None, "viewport configuration"),
|
||||
];
|
||||
|
||||
/// Keyword-only: spaces separate inputs.
|
||||
const KW: Accept = Accept { point: false, number: false, text: false, select: false, enter: true };
|
||||
/// A name: Space ends the input.
|
||||
const NAME: Accept = Accept { number: true, ..Accept::TEXT };
|
||||
|
||||
fn any_named<'a>(mut names: impl Iterator<Item = &'a String>, name: &str) -> bool {
|
||||
names.any(|n| n.eq_ignore_ascii_case(name))
|
||||
}
|
||||
|
||||
fn exists(d: &Drawing, table: &str, name: &str) -> bool {
|
||||
match table {
|
||||
"block" => d.block(name).is_some(),
|
||||
"layer" => d.layer(name).is_some(),
|
||||
"linetype" => d.linetype(name).is_some(),
|
||||
"style" => d.text_style(name).is_some(),
|
||||
"dimstyle" => d.dim_style(name).is_some(),
|
||||
"mleaderstyle" => any_named(d.mleader_styles.iter().map(|t| &t.name), name),
|
||||
"tablestyle" => any_named(d.table_styles.iter().map(|t| &t.name), name),
|
||||
"view" => any_named(d.views.iter().map(|t| &t.name), name),
|
||||
"ucs" => any_named(d.ucss.iter().map(|t| &t.name), name),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
enum Stage {
|
||||
#[default]
|
||||
Type,
|
||||
Old(usize),
|
||||
New(usize, String),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(in crate::cmd) struct RenameM {
|
||||
stage: Stage,
|
||||
}
|
||||
|
||||
fn text(i: &Input) -> Option<String> {
|
||||
match i {
|
||||
Input::Text(t) | Input::Keyword(t) => Some(t.trim().to_string()).filter(|t| !t.is_empty()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Interactive for RenameM {
|
||||
fn name(&self) -> &'static str {
|
||||
"-RENAME"
|
||||
}
|
||||
fn prompt(&self, _s: &Session) -> Prompt {
|
||||
let noun = |t: &usize| TYPES.get(*t).map_or("object", |(_, _, n)| *n);
|
||||
match &self.stage {
|
||||
Stage::Type => {
|
||||
let kws: Vec<&str> = TYPES.iter().map(|(k, _, _)| *k).collect();
|
||||
Prompt::new("Enter object type to rename", KW).kw(&kws)
|
||||
}
|
||||
Stage::Old(t) => Prompt::new(format!("Enter old {} name", noun(t)), NAME),
|
||||
Stage::New(t, _) => Prompt::new(format!("Enter new {} name", noun(t)), NAME),
|
||||
}
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if matches!(i, Input::Cancel | Input::Enter) {
|
||||
return Ok(if i == Input::Cancel { Step::Cancel } else { Step::Done });
|
||||
}
|
||||
match std::mem::take(&mut self.stage) {
|
||||
Stage::Type => {
|
||||
let t = match &i {
|
||||
Input::Keyword(k) => TYPES.iter().position(|(kw, _, _)| kw == k),
|
||||
_ => None,
|
||||
};
|
||||
let Some(t) = t else { return Err(EngineError::Other("Invalid option keyword.".into())) };
|
||||
self.stage = Stage::Old(t);
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
Stage::Old(t) => {
|
||||
let (Some(name), Some((_, table, noun))) = (text(&i), TYPES.get(t)) else {
|
||||
self.stage = Stage::Old(t);
|
||||
return Ok(Step::Continue);
|
||||
};
|
||||
if let Some(table) = table
|
||||
&& !exists(s.doc()?, table, &name)
|
||||
{
|
||||
self.stage = Stage::Old(t);
|
||||
return Err(EngineError::Other(format!("No {noun} named \"{name}\".")));
|
||||
}
|
||||
self.stage = Stage::New(t, name);
|
||||
Ok(Step::Continue)
|
||||
}
|
||||
Stage::New(t, old) => {
|
||||
let (Some(name), Some((_, table, noun))) = (text(&i), TYPES.get(t)) else {
|
||||
self.stage = Stage::New(t, old);
|
||||
return Ok(Step::Continue);
|
||||
};
|
||||
let Some(table) = table else {
|
||||
s.echo(format!("Renaming a {noun} is not available yet."));
|
||||
return Ok(Step::Done);
|
||||
};
|
||||
match super::run_rename(s, &json!({ "table": table, "from": old, "to": name })) {
|
||||
Ok(_) => Ok(Step::Done),
|
||||
Err(e) => {
|
||||
self.stage = Stage::New(t, old);
|
||||
Err(EngineError::Other(match e {
|
||||
EngineError::BadParams { msg, .. } => msg,
|
||||
other => other.to_string(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
//! 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);
|
||||
}
|
||||
Loading…
Reference in New Issue