mirror of
https://github.com/storytold/cadcraft.git
synced 2026-11-01 23:06:50 -06:00
BLOCKREPLACE: Tools > Express Tools > Replace Block replaces every reference of one block with another
BLOCKREPLACE (and -BLOCKREPLACE) turns every reference of one block, in model space, the layouts and other blocks, into a reference of another block at the same insertion point, scale and rotation. Each new reference carries the replacement's attributes, with the values of the tags both blocks define, and is annotative when the replacement is. The command then asks whether to purge the replaced definition, and removes it only when no dimension style names it and no block reference, dimension or multileader in the drawing or in another block definition does. The command refuses a replacement that references the block it replaces, a dynamic block as the block to replace, and blocks that come from an xref. It does not run while the Block Editor or REFEDIT is open. The ATTSYNC helpers in blocks/attribs.rs walk the references and build the new attributes. The self-reference check that BLOCK, INSERT of a file, REFCLOSE and Lisp block creation share now also follows dimension blocks and multileader block content, so BLOCK no longer accepts a redefinition of A from a multileader whose content is A. docs/parity-checklist.md is regenerated.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"]);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -785,6 +785,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 إعادة تعيين تخطيط اللوحات
|
||||
|
||||
|
@@ -730,6 +730,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
|
||||
|
||||
|
@@ -622,6 +622,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
|
||||
|
||||
|
@@ -852,6 +852,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 Επαναφορά παλετών
|
||||
|
||||
|
@@ -828,6 +828,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
|
||||
|
||||
|
@@ -616,6 +616,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
|
||||
|
||||
|
@@ -861,6 +861,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.
|
@@ -742,6 +742,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
|
||||
|
||||
|
@@ -732,6 +732,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 복원
|
||||
|
||||
|
@@ -820,6 +820,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
|
||||
|
||||
|
@@ -616,6 +616,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 Восстановить
|
||||
|
||||
|
@@ -824,6 +824,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
|
||||
|
||||
|
@@ -862,6 +862,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.
|
@@ -748,6 +748,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 Відновити
|
||||
|
||||
|
@@ -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,11 +1,10 @@
|
||||
# Parity checklist
|
||||
|
||||
> **Generated:** 2026-10-15 by `cargo xtask parity`. Do not edit by hand.
|
||||
> **Generated:** 2026-10-16 by `cargo xtask parity`. Do not edit by hand.
|
||||
> **Generated:** 2026-10-24 by `cargo xtask parity`. Do not edit by hand.
|
||||
|
||||
Menu-breadth parity: a reference menu item counts as covered when a CADCraft command is registered under the same menu label. It measures breadth, not depth; see [target-app-parity.md](target-app-parity.md) for the weighted assessment.
|
||||
|
||||
**Menu breadth: 283 / 491 items (58%)** · 427 registered commands.
|
||||
**Menu breadth: 288 / 491 items (59%)** · 434 registered commands.
|
||||
|
||||
| Menu | Items | Covered | % |
|
||||
|---|---|---|---|
|
||||
@@ -14,7 +13,7 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com
|
||||
| View | 75 | 31 | 41% |
|
||||
| Insert | 11 | 5 | 45% |
|
||||
| Format | 27 | 25 | 93% |
|
||||
| Tools | 85 | 60 | 71% |
|
||||
| Tools | 85 | 65 | 76% |
|
||||
| Draw | 87 | 55 | 63% |
|
||||
| Dimension | 28 | 28 | 100% |
|
||||
| Modify | 113 | 52 | 46% |
|
||||
@@ -101,14 +100,8 @@ Menu-breadth parity: a reference menu item counts as covered when a CADCraft com
|
||||
- Tools > New UCS > Z
|
||||
- Tools > Named UCS...
|
||||
- Tools > Customize > Interface (CUI)...
|
||||
- Tools > Customize > Edit Command Aliases (PGP)...
|
||||
- Tools > Import Settings
|
||||
- Tools > Export Settings
|
||||
- Tools > Express Tools > Convert Text to Mtext
|
||||
- Tools > Express Tools > Break-line Symbol
|
||||
- Tools > Express Tools > Move/Copy/Rotate
|
||||
- Tools > Express Tools > Explode Attributes to Text
|
||||
- Tools > Express Tools > Replace Block...
|
||||
- Tools > Express Tools > Super Hatch > Image...
|
||||
- Tools > Express Tools > Super Hatch > Xref...
|
||||
- Tools > Express Tools > Super Hatch > Select Existing Object
|
||||
|
||||
Reference in New Issue
Block a user