# Conflicts:
#	docs/parity-checklist.md
This commit is contained in:
aiongg
2026-10-25 02:22:16 +00:00
22 changed files with 460 additions and 15 deletions
+12 -3
View File
@@ -74,6 +74,7 @@ pub fn specs() -> Vec<CommandSpec> {
attribs::attdisp_spec(),
attribs::attsync_spec(),
burst::burst_spec(),
blockreplace::blockreplace_spec(),
extract::dataextraction_spec(),
extract::eattext_spec(),
]
@@ -88,12 +89,19 @@ pub(crate) fn valid_block_name(n: &str) -> bool {
!n.trim().is_empty() && n.len() <= 255 && !n.starts_with('*') && !n.chars().any(|c| "<>/\\\":;?*|,=`".contains(c))
}
/// The block an entity references, when it is a block reference.
/// The block an entity draws, if any: a block reference's block, a dimension's block or a
/// multileader's block content.
fn referenced_block(e: &Entity) -> Option<String> {
if let EntityKind::Insert(i) = &e.kind { Some(i.block.clone()) } else { None }
match &e.kind {
EntityKind::Insert(i) => Some(i.block.clone()),
EntityKind::Dimension(dm) => dm.block.clone(),
EntityKind::MLeader(m) => m.block.as_ref().map(|b| b.block.clone()),
_ => None,
}
}
/// Whether any of `ents` refers to block `name`, directly or through nested block references.
/// Whether any of `ents` refers to block `name`, directly or through nested block references,
/// dimension blocks and multileader block content.
pub(crate) fn references_block(d: &cadcraft_doc::Drawing, ents: &[Entity], name: &str) -> bool {
let mut todo: Vec<String> = ents.iter().filter_map(referenced_block).collect();
let mut seen = std::collections::HashSet::new();
@@ -878,3 +886,4 @@ impl Interactive for AttdefM {
mod attribs;
mod battman;
mod blockreplace;
+19 -11
View File
@@ -84,7 +84,7 @@ impl Interactive for AttdispM {
/// The attributes `ins` gets from the definitions `defs` of its block `blk`: placed, styled and
/// shown as defined, keeping the values of attributes whose tag (any case) is still defined.
/// Properties set on single attributes (layer, colour, …) go back to the definitions', as in AutoCAD.
fn synced(blk: &Block, defs: &[&Attrib], ins: &Insert) -> Vec<Attrib> {
pub(super) fn synced(blk: &Block, defs: &[&Attrib], ins: &Insert) -> Vec<Attrib> {
let m = ins.transform(blk.base.xy());
defs.iter()
.map(|ad| {
@@ -119,12 +119,16 @@ fn references<'a>(ents: impl Iterator<Item = &'a Arc<Entity>>, name: &str) -> Ve
/// Apply `f` to every reference of block `name` (in model space, layouts and other blocks);
/// returns how many references there were.
fn update_references(d: &mut cadcraft_doc::Drawing, name: &str, mut f: impl FnMut(&mut Insert)) -> usize {
let mut n = 0;
let mut update = |e: &mut Entity| {
update_reference_entities(d, name, |e| {
if let EntityKind::Insert(ins) = &mut e.kind {
f(ins);
}
};
})
}
/// [`update_references`], with `f` given the whole object.
pub(super) fn update_reference_entities(d: &mut cadcraft_doc::Drawing, name: &str, mut update: impl FnMut(&mut Entity)) -> usize {
let mut n = 0;
let spaces: Vec<cadcraft_doc::Space> =
std::iter::once(cadcraft_doc::Space::Model).chain(d.layouts.iter().map(|l| cadcraft_doc::Space::Paper(l.name.clone()))).collect();
for sp in spaces {
@@ -169,18 +173,22 @@ pub(super) fn rename_tag(s: &mut Session, name: &str, old: &str, new: &str) -> R
}))
}
/// Synchronize every reference of block `name` (in model space, layouts and other blocks).
/// Returns the block's name as defined and how many references were updated.
pub(super) fn sync(s: &mut Session, name: &str) -> Result<(String, usize)> {
let blk = s.doc()?.block(name).cloned().ok_or_else(|| EngineError::Other(format!("Block \"{name}\" not found.")))?;
let defs: Vec<&Attrib> = blk
.entities
/// The attribute definitions of `blk` that references carry (constant ones are not).
pub(super) fn attdefs(blk: &Block) -> Vec<&Attrib> {
blk.entities
.iter()
.filter_map(|e| match &e.kind {
EntityKind::AttDef(a) if !a.constant => Some(a),
_ => None,
})
.collect();
.collect()
}
/// Synchronize every reference of block `name` (in model space, layouts and other blocks).
/// Returns the block's name as defined and how many references were updated.
pub(super) fn sync(s: &mut Session, name: &str) -> Result<(String, usize)> {
let blk = s.doc()?.block(name).cloned().ok_or_else(|| EngineError::Other(format!("Block \"{name}\" not found.")))?;
let defs = attdefs(&blk);
let d = s.doc_mut()?;
let n = update_references(d, &blk.name, |ins| ins.attribs = synced(&blk, &defs, ins));
Ok((blk.name.clone(), n))
@@ -0,0 +1,220 @@
//! BLOCKREPLACE and -BLOCKREPLACE (Express Tools): every reference of one block, in model space,
//! the layouts and other blocks, becomes a reference of another block at the same insertion
//! point, scale and rotation. The unreferenced block definition can then be purged.
use cadcraft_doc::{Drawing, Entity, EntityKind};
use serde_json::{Value, json};
use super::attribs::{attdefs, synced, update_reference_entities};
use super::purge::{override_arrows, used_blocks};
use super::references_block;
use crate::cmd::curves::KW;
use crate::cmd::machines::{SelOutcome, SelectPhase};
use crate::cmd::{CommandSpec, bad, str_param};
use crate::{Accept, EngineError, Input, Interactive, Prompt, Result, Session, Step};
pub(super) fn blockreplace_spec() -> CommandSpec {
CommandSpec::new("blockreplace", "Replace Block...", run_blockreplace)
.menu(&["Tools", "Express Tools", "Replace Block..."])
.alias(&["-blockreplace"])
.params("{block (the block whose references are replaced), with (the replacement block), purge?: bool (default false: remove the replaced block's definition when nothing references it any more)} → {block, with, replaced, purged}")
.interactive(|_| Ok(Box::new(BlockreplaceM::default())))
}
/// The defined name of block `name` (names are case-insensitive), when it is a named block of
/// this drawing: not anonymous and not one an xref brings in (`X|A`), which is never saved.
fn named_block(d: &Drawing, name: &str) -> Option<String> {
d.block(name.trim()).filter(|b| !b.anonymous && !b.name.starts_with('*') && !b.name.contains('|')).map(|b| b.name.clone())
}
/// Replace every reference of `old` by a reference of `new`; with `purge`, remove `old`'s
/// definition when nothing references it afterwards. Returns (references replaced, purged).
fn replace(s: &mut Session, old: &str, new: &str, purge: bool) -> Result<(usize, bool)> {
let cmd = "blockreplace";
if s.state()?.block_edit.is_some() {
return Err(bad(cmd, "the Block Editor is open; close it with BCLOSE first"));
}
let d = s.doc()?;
if d.refedit.is_some() {
return Err(bad(cmd, "a block is being edited in place; finish with REFCLOSE first"));
}
if let Some(n) = [old, new].into_iter().find(|n| n.contains('|')) {
return Err(bad(cmd, format!("Block \"{}\" comes from an xref; bind the xref first.", n.trim())));
}
let old = named_block(d, old).ok_or_else(|| bad(cmd, format!("Block \"{}\" not found.", old.trim())))?;
let new = named_block(d, new).ok_or_else(|| bad(cmd, format!("Block \"{}\" not found.", new.trim())))?;
if old.eq_ignore_ascii_case(&new) {
return Err(bad(cmd, "the replacement block must be a different block"));
}
// A dynamic block's references keep their property values in data that names the block.
if cadcraft_doc::dynblock::dynamic_block(d, &old).is_some() {
return Err(bad(cmd, format!("Block \"{old}\" is a dynamic block; its references cannot be replaced.")));
}
let blk = d.block(&new).cloned().ok_or_else(|| bad(cmd, format!("Block \"{new}\" not found.")))?;
// References of `old` inside `new` would make `new` reference itself.
if references_block(d, &blk.entities.iter().map(|e| (**e).clone()).collect::<Vec<_>>(), &old) {
return Err(bad(cmd, format!("Block \"{new}\" references \"{old}\", so it cannot replace it.")));
}
let defs = attdefs(&blk);
let scale = d.current_anno_scale();
let d = s.doc_mut()?;
let n = update_reference_entities(d, &old, |e| {
if let EntityKind::Insert(ins) = &mut e.kind {
ins.block.clone_from(&blk.name);
ins.attribs = synced(&blk, &defs, ins);
}
// A reference of an annotative block is annotative, as INSERT makes it.
if !blk.annotative {
e.common.anno_scales.clear();
} else if e.common.anno_scales.is_empty() {
e.common.anno_scales = vec![scale.clone()];
}
});
let key = d.blocks.keys().find(|k| k.eq_ignore_ascii_case(&old)).cloned();
let purged = purge && !referenced(d, &old) && key.is_some_and(|k| d.blocks.remove(&k).is_some());
Ok((n, purged))
}
/// Whether anything still names block `name`: what PURGE counts as in use, and the dimensions,
/// multileaders and block references of every other block definition, used or not.
fn referenced(d: &Drawing, name: &str) -> bool {
if used_blocks(d).contains(&name.to_ascii_uppercase()) {
return true;
}
let names = |e: &Entity| -> Vec<String> {
match &e.kind {
EntityKind::Insert(i) => vec![i.block.clone()],
EntityKind::Dimension(dm) => dm.block.iter().cloned().chain(override_arrows(dm)).collect(),
EntityKind::MLeader(m) => m.block.iter().map(|b| b.block.clone()).collect(),
_ => Vec::new(),
}
};
d.blocks
.values()
.filter(|b| !b.name.eq_ignore_ascii_case(name))
.flat_map(|b| b.entities.iter())
.any(|e| names(e).iter().any(|n| n.trim().eq_ignore_ascii_case(name)))
}
fn run_blockreplace(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "blockreplace";
let old = str_param(p, "block").ok_or_else(|| bad(cmd, "`block` (the block to replace) is required"))?;
let new = str_param(p, "with").ok_or_else(|| bad(cmd, "`with` (the replacement block) is required"))?;
let purge = match p.get("purge") {
None | Some(Value::Null) => false,
Some(Value::Bool(b)) => *b,
Some(v) => return Err(bad(cmd, format!("`purge` must be true or false, not {v}"))),
};
let (old, new) = (old.to_string(), new.to_string());
let (n, purged) = replace(s, &old, &new, purge)?;
let d = s.doc()?;
let defined = |name: &str| named_block(d, name).unwrap_or_else(|| name.trim().to_string());
Ok(json!({ "block": defined(&old), "with": defined(&new), "replaced": n, "purged": purged }))
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
enum Stage {
#[default]
Old,
New,
Select,
Purge,
}
#[derive(Default)]
struct BlockreplaceM {
stage: Stage,
/// The stage a picked reference answers.
back: Stage,
sel: SelectPhase,
old: String,
new: String,
}
impl BlockreplaceM {
/// Take `name` as the answer to the current name prompt.
fn take(&mut self, s: &Session, stage: Stage, name: &str) -> Result<()> {
let d = s.doc()?;
let name = named_block(d, name).ok_or_else(|| EngineError::Other(format!("Block \"{}\" not found.", name.trim())))?;
if stage == Stage::Old {
self.old = name;
self.stage = Stage::New;
} else {
if name.eq_ignore_ascii_case(&self.old) {
return Err(EngineError::Other("The replacement block must be a different block.".into()));
}
self.new = name;
self.stage = Stage::Purge;
}
Ok(())
}
fn finish(&mut self, s: &mut Session, purge: bool) -> Result<Step> {
let (n, purged) = replace(s, &self.old, &self.new, purge)?;
s.echo(format!(
"{n} reference{} of block {} replaced with {}.{}",
if n == 1 { "" } else { "s" },
self.old,
self.new,
if purged { format!(" Block {} purged.", self.old) } else { String::new() }
));
Ok(Step::Done)
}
}
impl Interactive for BlockreplaceM {
fn name(&self) -> &'static str {
"BLOCKREPLACE"
}
fn prompt(&self, _s: &Session) -> Prompt {
match self.stage {
Stage::Old => Prompt::new("Enter the name of the block to replace", Accept::TEXT).kw(&["?", "Select"]),
Stage::New => Prompt::new(format!("Enter the name of the replacement block for {}", self.old), Accept::TEXT).kw(&["?", "Select"]),
Stage::Select => Prompt::new("Select a block reference", Accept::SELECT),
Stage::Purge => Prompt::new(format!("Purge the unreferenced block definition {}?", self.old), KW).kw(&["Yes", "No"]).default("Yes"),
}
}
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
match self.stage {
stage @ (Stage::Old | Stage::New) => match &i {
Input::Keyword(k) if k == "?" => {
let d = s.doc()?;
let mut names: Vec<String> = d.blocks.keys().filter_map(|k| named_block(d, k)).collect();
names.sort_by_key(|n| n.to_ascii_lowercase());
s.echo(if names.is_empty() { "No blocks defined.".to_string() } else { format!("Blocks: {}", names.join(", ")) });
}
Input::Keyword(k) if k == "Select" => {
self.back = stage;
self.sel = SelectPhase::single();
self.stage = Stage::Select;
}
Input::Text(t) if !t.trim().is_empty() => self.take(s, stage, t)?,
Input::Enter | Input::Cancel => return Ok(Step::Done),
_ => {}
},
Stage::Select => match self.sel.feed(s, &i)? {
SelOutcome::More => {}
SelOutcome::Empty => self.stage = self.back,
SelOutcome::Done(hs) => {
s.set_selection(Vec::new());
self.sel = SelectPhase::single();
let block = hs.iter().find_map(|h| match s.doc().ok()?.entity(*h).map(|e| &e.kind) {
Some(EntityKind::Insert(ins)) => Some(ins.block.clone()),
_ => None,
});
match block {
Some(b) => self.take(s, self.back, &b)?,
None => return Err(EngineError::Other("That object is not a block reference.".into())),
}
}
},
Stage::Purge => match &i {
Input::Keyword(k) if k == "No" => return self.finish(s, false),
Input::Keyword(_) | Input::Enter => return self.finish(s, true),
Input::Cancel => return Ok(Step::Done),
_ => return Err(EngineError::Other("Invalid option keyword.".into())),
},
}
Ok(Step::Continue)
}
}
+1 -1
View File
@@ -119,7 +119,7 @@ fn entities(d: &Drawing) -> impl Iterator<Item = &std::sync::Arc<cadcraft_doc::E
}
/// The arrowhead block names a dimension's overrides set.
fn override_arrows(dm: &cadcraft_doc::Dimension) -> impl Iterator<Item = String> + '_ {
pub(in crate::cmd) fn override_arrows(dm: &cadcraft_doc::Dimension) -> impl Iterator<Item = String> + '_ {
dm.overrides
.iter()
.filter(|(k, _)| cadcraft_doc::DimStyle::field_name(k).is_some_and(|f| f.starts_with("arrowBlock") || f == "leaderArrowBlock"))
+6
View File
@@ -40,6 +40,12 @@ fn a_block_cannot_be_redefined_to_reference_itself() {
let r = s.execute("block", &json!({ "name": "a", "base": [0, 0], "handles": [b] }));
assert!(r.is_err_and(|e| e.to_string().contains("references itself")));
assert_eq!(contents(&s, "A"), ["line"]);
// Through a multileader whose content is the block being redefined.
let m = s.execute("mleader", &json!({ "points": [[0, 0], [10, 5]], "block": "A" })).unwrap()["handle"].clone();
let r = s.execute("block", &json!({ "name": "A", "base": [0, 0], "handles": [m] }));
assert!(r.is_err_and(|e| e.to_string().contains("references itself")));
assert_eq!(contents(&s, "A"), ["line"]);
}
#[test]
+186
View File
@@ -0,0 +1,186 @@
//! BLOCKREPLACE (Express Tools): every reference of one block, in model space, the layouts and
//! other blocks, becomes a reference of another at the same insertion point, scale and rotation.
use cadcraft_engine::doc::geom::Vec3;
use cadcraft_engine::doc::{Block, Common, Entity, EntityKind, Handle, Insert, Space};
use cadcraft_engine::{Input, Session};
use serde_json::{Value, json};
/// Make block `name` from what is in model space now (the objects are removed).
fn make_block(s: &mut Session, name: &str) {
let hs: Vec<String> = s.doc().unwrap().model.iter().map(|e| e.handle.hex()).collect();
s.execute("block", &json!({"name": name, "base": [0, 0], "handles": hs, "keep": "delete"})).unwrap();
}
/// Block A: a circle and attribute TAG. Block B: a line, attributes TAG and NEW. Block N holds a
/// reference of A. Model space: a reference of A on layer R at (10, 5), scaled 2 and rotated
/// 30°, with TAG "kept", and a reference of N. Layout1: a reference of A.
fn drawing() -> (Session, Handle) {
let mut s = Session::new();
s.execute("attdef", &json!({"tag": "TAG", "default": "a", "at": [0, 2], "height": 1.0})).unwrap();
s.execute("circle", &json!({"center": [0, 0], "radius": 1})).unwrap();
make_block(&mut s, "A");
s.execute("attdef", &json!({"tag": "TAG", "default": "b", "at": [0, 3], "height": 1.0})).unwrap();
s.execute("attdef", &json!({"tag": "NEW", "default": "new", "at": [0, 5], "height": 1.0})).unwrap();
s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap();
make_block(&mut s, "B");
s.execute("insert", &json!({"name": "A", "at": [0, 0]})).unwrap();
make_block(&mut s, "N");
s.execute("insert", &json!({"name": "N", "at": [50, 0]})).unwrap();
s.execute("layer.new", &json!({"name": "R", "current": true})).unwrap();
let r = s.execute("insert", &json!({"name": "A", "at": [10, 5], "scale": 2, "rotation": 30, "attribs": {"TAG": "kept"}})).unwrap();
let ins = Insert {
block: "A".into(),
insert: Vec3::new(0.0, 0.0, 0.0),
scale: Vec3::new(1.0, 1.0, 1.0),
rotation: 0.0,
attribs: Vec::new(),
cols: 1,
rows: 1,
col_spacing: 0.0,
row_spacing: 0.0,
};
s.doc_mut().unwrap().add(&Space::Paper("Layout1".into()), Common::default(), EntityKind::Insert(ins)).unwrap();
(s, Handle::parse_hex(r["handle"].as_str().unwrap()).unwrap())
}
/// The block names of the references in `ents`.
fn refs<'a>(ents: impl Iterator<Item = &'a std::sync::Arc<Entity>>) -> Vec<String> {
ents.filter_map(|e| match &e.kind {
EntityKind::Insert(i) => Some(i.block.clone()),
_ => None,
})
.collect()
}
fn insert(s: &Session, h: Handle) -> (Insert, String) {
let e = s.doc().unwrap().entity(h).unwrap();
let EntityKind::Insert(i) = &e.kind else { panic!("not a block reference") };
(i.clone(), e.common.layer.clone())
}
#[test]
fn blockreplace_replaces_every_reference_and_keeps_its_placement() {
let (mut s, h) = drawing();
let before = insert(&s, h).0;
let r = s.execute("blockreplace", &json!({"block": "a", "with": "b"})).unwrap();
assert_eq!(r, json!({"block": "A", "with": "B", "replaced": 3, "purged": false}));
let (after, layer) = insert(&s, h);
assert_eq!(after.block, "B");
assert_eq!((after.insert, after.scale, after.rotation, layer.as_str()), (before.insert, before.scale, before.rotation, "R"));
// The replacement's attributes, with the value of the tag both blocks define.
let values: Vec<(String, String)> = after.attribs.iter().map(|a| (a.tag.clone(), a.text.value.clone())).collect();
assert_eq!(values, [("TAG".to_string(), "kept".to_string()), ("NEW".to_string(), "new".to_string())]);
let d = s.doc().unwrap();
assert_eq!(refs(d.model.iter()), ["N", "B"]);
assert_eq!(refs(d.block("N").unwrap().entities.iter()), ["B"]);
assert_eq!(refs(d.space(&Space::Paper("Layout1".into())).unwrap().iter()), ["B"]);
assert!(d.block("A").is_some(), "the definition stays without `purge`");
s.execute("undo", &json!({})).unwrap();
assert_eq!(insert(&s, h).0, before);
}
#[test]
fn blockreplace_references_follow_the_replacement_blocks_annotative_setting() {
let (mut s, h) = drawing();
let c = s.execute("circle", &json!({"center": [0, 0], "radius": 2})).unwrap()["handle"].clone();
s.execute("block", &json!({"name": "AN", "base": [0, 0], "handles": [c], "keep": "delete", "annotative": true})).unwrap();
let scales = |s: &Session| s.doc().unwrap().entity(h).unwrap().common.anno_scales.len();
assert_eq!(scales(&s), 0);
s.execute("blockreplace", &json!({"block": "A", "with": "AN"})).unwrap();
assert_eq!(scales(&s), 1, "a reference of an annotative block supports the current scale");
s.execute("blockreplace", &json!({"block": "AN", "with": "B"})).unwrap();
assert_eq!(scales(&s), 0);
}
#[test]
fn blockreplace_purges_the_replaced_definition_on_request() {
let (mut s, _) = drawing();
let r = s.execute("blockreplace", &json!({"block": "A", "with": "B", "purge": true})).unwrap();
assert_eq!(r["purged"], json!(true));
assert!(s.doc().unwrap().block("A").is_none());
}
#[test]
fn blockreplace_keeps_a_definition_an_unused_block_still_names() {
let (mut s, _) = drawing();
// Block U, never inserted, holds a multileader whose content is block A.
let m = s.execute("mleader", &json!({"points": [[0, 0], [10, 5]], "block": "A"})).unwrap()["handle"].clone();
s.execute("block", &json!({"name": "U", "base": [0, 0], "handles": [m], "keep": "delete"})).unwrap();
let r = s.execute("blockreplace", &json!({"block": "A", "with": "B", "purge": true})).unwrap();
assert_eq!((r["replaced"].clone(), r["purged"].clone()), (json!(3), json!(false)));
assert!(s.doc().unwrap().block("A").is_some());
}
#[test]
fn blockreplace_refuses_what_it_cannot_replace() {
let (mut s, _) = drawing();
let err = |s: &mut Session, p: Value| s.execute("blockreplace", &p).unwrap_err().to_string();
assert!(err(&mut s, json!({"block": "A", "with": "a"})).contains("different block"));
assert!(err(&mut s, json!({"block": "A", "with": "MISSING"})).contains("not found"));
assert!(err(&mut s, json!({"block": "MISSING", "with": "B"})).contains("not found"));
// N holds a reference of A, so N replacing A would reference itself.
assert!(err(&mut s, json!({"block": "A", "with": "N"})).contains("references"));
// M holds a multileader whose content is N, so M reaches A too.
let m = s.execute("mleader", &json!({"points": [[0, 0], [10, 5]], "block": "N"})).unwrap()["handle"].clone();
s.execute("block", &json!({"name": "M", "base": [0, 0], "handles": [m], "keep": "delete"})).unwrap();
assert!(err(&mut s, json!({"block": "A", "with": "M"})).contains("references"));
assert!(err(&mut s, json!({"block": "A", "with": "B", "purge": "yes"})).contains("`purge`"));
assert!(err(&mut s, json!({"with": "B"})).contains("`block`"));
// A block an xref brings in is not saved with the drawing.
s.doc_mut().unwrap().blocks.insert("X|A".into(), std::sync::Arc::new(Block::new("X|A")));
assert!(err(&mut s, json!({"block": "X|A", "with": "B"})).contains("xref"));
assert!(err(&mut s, json!({"block": "A", "with": "X|A"})).contains("xref"));
assert_eq!(refs(s.doc().unwrap().model.iter()), ["N", "A"]);
// The Block Editor shows the block, not the drawing's references.
s.execute("bedit", &json!({"name": "B"})).unwrap();
assert!(err(&mut s, json!({"block": "A", "with": "B"})).contains("Block Editor"));
}
#[test]
fn blockreplace_asks_for_both_blocks_and_whether_to_purge() {
let (mut s, h) = drawing();
s.cmdline("-blockreplace").unwrap();
assert!(s.prompt_text().contains("Enter the name of the block to replace or [?/Select]"), "{}", s.prompt_text());
s.cmdline("?").unwrap();
assert!(s.log.iter().any(|l| l == "Blocks: A, B, N"), "{:?}", s.log);
s.cmdline("a").unwrap();
assert!(s.prompt_text().contains("Enter the name of the replacement block for A or [?/Select]"), "{}", s.prompt_text());
// A cannot replace itself: the prompt asks again.
s.cmdline("A").unwrap();
assert!(s.prompt_text().contains("replacement block for A"), "{}", s.prompt_text());
s.input(Input::Cancel).unwrap();
assert!(s.running.is_none());
assert_eq!(refs(s.doc().unwrap().model.iter()), ["N", "A"]);
// The block to replace picked from one of its references; Enter purges it.
s.cmdline("blockreplace").unwrap();
s.cmdline("Select").unwrap();
s.input(Input::Pick(vec![h])).unwrap();
assert!(s.prompt_text().contains("replacement block for A"), "{}", s.prompt_text());
s.cmdline("B").unwrap();
assert!(s.prompt_text().contains("Purge the unreferenced block definition A? [Yes/No] <Yes>"), "{}", s.prompt_text());
s.cmdline("").unwrap();
assert!(s.running.is_none());
assert!(s.log.iter().any(|l| l == "3 references of block A replaced with B. Block A purged."), "{:?}", s.log);
let d = s.doc().unwrap();
assert!(d.block("A").is_none());
assert_eq!(refs(d.model.iter()), ["N", "B"]);
assert_eq!(insert(&s, h).0.block, "B");
}
#[test]
fn blockreplace_no_keeps_the_replaced_definition() {
let (mut s, _) = drawing();
s.cmdline("blockreplace").unwrap();
s.cmdline("A").unwrap();
s.cmdline("B").unwrap();
s.cmdline("No").unwrap();
assert!(s.running.is_none());
let d = s.doc().unwrap();
assert!(d.block("A").is_some());
assert_eq!(refs(d.model.iter()), ["N", "B"]);
}
+1
View File
@@ -12,6 +12,7 @@ mod attrib_props;
mod attribute_tools;
mod block_keep;
mod block_redefine;
mod blockreplace;
mod break_closed_polyline;
mod break_join_locked_layers;
mod break_keeps_z;
+1
View File
@@ -786,6 +786,7 @@
Rename Text Style إعادة تسمية نمط النص
Rename... إعادة تسمية...
Repeat {command} تكرار {command}
Replace Block... استبدال الكتلة...
Requires a point (x,y, @dx,dy or @d<a). يتطلب نقطة (x,y أو @dx,dy أو @d<a).
Reset Palettes إعادة تعيين اللوحات
Reset Panel Layout إعادة تعيين تخطيط اللوحات
1 # CADCraft Arabic interface catalog — original translations.
786
787
788
789
790
791
792
+1
View File
@@ -731,6 +731,7 @@
Rename Text Style Přejmenovat styl textu
Rename... Přejmenovat...
Repeat {command} Opakovat {command}
Replace Block... Nahradit blok...
Requires a point (x,y, @dx,dy or @d<a). Je vyžadován bod (x,y, @dx,dy nebo @d<a).
Reset Palettes Obnovit palety
Restore Obnovit
1 # CADCraft Czech interface catalog — original translations by @Losminek (AI-assisted).
731
732
733
734
735
736
737
+1
View File
@@ -623,6 +623,7 @@
Rename Text Style Textstil umbenennen
Rename... Umbenennen...
Repeat {command} {command} wiederholen
Replace Block... Block ersetzen...
Requires a point (x,y, @dx,dy or @d<a). Erfordert einen Punkt (x,y, @dx,dy oder @d<a).
Reset Palettes Paletten zurücksetzen
Restore Wiederherstellen
1 # CADCraft German interface catalog — original translations by the CADCraft contributors.
623
624
625
626
627
628
629
+1
View File
@@ -853,6 +853,7 @@
Rename Text Style Μετονομασία στυλ κειμένου
Rename... Μετονομασία...
Repeat {command} Επανάληψη εντολής {command}
Replace Block... Αντικατάσταση μπλοκ...
Requires a point (x,y, @dx,dy or @d<a). Απαιτείται σημείο (x,y, @dx,dy ή @d<a).
Reset keeps the interface language. Η επαναφορά διατηρεί τη γλώσσα του περιβάλλοντος.
Reset Palettes Επαναφορά παλετών
1 # Greek interface catalog for CADCraft. Original, AI-assisted translation by @Camthalion77 with ChatGPT.
853
854
855
856
857
858
859
+1
View File
@@ -829,6 +829,7 @@
Rename Text Style Renombrar estilo de texto
Rename... Renombrar...
Repeat {command} Repetir {command}
Replace Block... Reemplazar bloque...
Requires a point (x,y, @dx,dy or @d<a). Se requiere un punto (x,y, @dx,dy o @d<a).
Reset Palettes Restablecer paletas
Restore Restaurar
1 # CADCraft Spanish interface catalog — original translations.
829
830
831
832
833
834
835
+1
View File
@@ -617,6 +617,7 @@
Rename Text Style Renommer le style de texte
Rename... Renommer...
Repeat {command} Répéter {command}
Replace Block... Remplacer le bloc...
Requires a point (x,y, @dx,dy or @d<a). Nécessite un point (x,y, @dx,dy ou @d<a).
Reset Palettes Réinitialiser les palettes
Restore Restaurer
1 # CADCraft French interface catalog — original translations by @pechnikov2026-star.
617
618
619
620
621
622
623
+1
View File
@@ -862,6 +862,7 @@
Rename Text Style Szövegstílus átnevezése
Rename... Átnevezés...
Repeat {command} Ismétlés: {command}
Replace Block... Blokk cseréje...
Requires a point (x,y, @dx,dy or @d<a). Pontot kell megadni (x,y, @dx,dy vagy @d<a).
Reset Palettes Paletták visszaállítása
Reset Panel Layout Panelelrendezés visszaállítása
Can't render this file because it contains an unexpected character in line 16 and column 72.
+1
View File
@@ -743,6 +743,7 @@ menu Modify Edita
Rename Text Style Rinomina stile di testo
Rename... Rinomina...
Repeat {command} Ripeti {command}
Replace Block... Sostituisci blocco...
Requires a point (x,y, @dx,dy or @d<a). Richiede un punto (x,y, @dx,dy o @d<a).
Reset Palettes Reimposta tavolozze
Restore Ripristina
1 # CADCraft Italian interface catalog — original translations by Emanuele Bottelli (AI-assisted, Claude).
743
744
745
746
747
748
749
+1
View File
@@ -733,6 +733,7 @@
Rename Text Style 문자 스타일 이름 바꾸기
Rename... 이름 바꾸기...
Repeat {command} {command} 반복
Replace Block... 블록 바꾸기...
Requires a point (x,y, @dx,dy or @d<a). 점이 필요합니다(x,y, @dx,dy 또는 @d<a).
Reset Palettes 팔레트 초기화
Restore 복원
1 # CADCraft Korean interface catalog — original Korean translations.
733
734
735
736
737
738
739
+1
View File
@@ -821,6 +821,7 @@
Rename Text Style Renomear estilo de texto
Rename... Renomear...
Repeat {command} Repetir {command}
Replace Block... Substituir bloco...
Requires a point (x,y, @dx,dy or @d<a). Requer um ponto (x,y, @dx,dy ou @d<a).
Reset Palettes Redefinir paletas
Restore Restaurar
1 # CADCraft Brazilian Portuguese (pt-BR) interface catalog.
821
822
823
824
825
826
827
+1
View File
@@ -617,6 +617,7 @@
Rename Text Style Переименовать текстовый стиль
Rename... Переименовать...
Repeat {command} Повторить {command}
Replace Block... Заменить блок...
Requires a point (x,y, @dx,dy or @d<a). Требуется точка (x,y, @dx,dy или @d<a).
Reset Palettes Сбросить палитры
Restore Восстановить
1 # CADCraft Russian interface catalog — original translations by @pechnikov2026-star.
617
618
619
620
621
622
623
+1
View File
@@ -825,6 +825,7 @@
Rename Text Style Byt namn på textstil
Rename... Byt namn...
Repeat {command} Upprepa {command}
Replace Block... Ersätt block...
Requires a point (x,y, @dx,dy or @d<a). Kräver en punkt (x,y, @dx,dy eller @d<a).
Reset Palettes Återställ paletter
Restore Återställ
1 # CADCraft Swedish interface catalog — original translations by the CADCraft contributors.
825
826
827
828
829
830
831
+1
View File
@@ -863,6 +863,7 @@
Rename Text Style Metin Stilini Yeniden Adlandır
Rename... Yeniden Adlandır...
Repeat {command} Yinele: {command}
Replace Block... Bloğu Değiştir...
Requires a point (x,y, @dx,dy or @d<a). Bir nokta gerekir (x,y, @dx,dy veya @d<a).
Reset Palettes Paletleri Sıfırla
Reset Panel Layout Panel Düzenini Sıfırla
Can't render this file because it contains an unexpected character in line 11 and column 13.
+1
View File
@@ -749,6 +749,7 @@
Rename Text Style Перейменувати текстовий стиль
Rename... Перейменувати...
Repeat {command} Повторити {command}
Replace Block... Замінити блок...
Requires a point (x,y, @dx,dy or @d<a). Потрібна точка (x,y, @dx,dy або @d<a).
Reset Palettes Скинути палітри
Restore Відновити
1 # CADCraft Ukrainian interface catalog — original translations by @dmatviichuk.
749
750
751
752
753
754
755
+1
View File
@@ -618,6 +618,7 @@
Rename Text Style 重命名文字样式
Rename... 重命名...
Repeat {command} 重复 {command}
Replace Block... 替换块...
Requires a point (x,y, @dx,dy or @d<a). 需要一个点 (x,y, @dx,dy 或 @d<a)。
Reset Palettes 重置选项板
Restore 恢复
1 # CADCraft Simplified Chinese interface catalog — original translations.
618
619
620
621
622
623
624