mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-29 05:16:49 -06:00
@@ -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;
|
||||
|
||||
@@ -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 إعادة تعيين تخطيط اللوحات
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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 Επαναφορά παλετών
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
@@ -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
|
||||
|
||||
|
@@ -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 복원
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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 Восстановить
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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.
|
@@ -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 Відновити
|
||||
|
||||
|
@@ -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 恢复
|
||||
|
||||
|
Reference in New Issue
Block a user