Browse / github.com/storytold/cadcraft / patch
wblock: keep the source units, drop unused blocks and avoid duplicate handles (#1996)
WBLOCK writes duplicate handles: AutoCAD refuses to open the file ("Bad handle 100: already in use") _Split as the triage bot asked: this ticket now covers only the duplicate handles. The other two problems have their own tickets: WBLOCK copies every block of the source drawing (#2003) and WBLOCK always writes `$INSUNITS = 1` (#2002). The original text is in this ticket's edit history._ **Environment:** CADCraft 0.12.0 (release MSI, `cadcraft-cli 0.12.0`), Windows 11 Pro. Interop observed in Autodesk AutoCAD 2026 (`accoreconsole`, opening a copy, never saving over it) — black-box observation only. All drawings are synthetic (written by CADCraft or by ezdxf); no AutoCAD-produced file is attac
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- MarekSpodBiedry (imported from pull request #2005, open)
- Base
f043540d13dac03ce895a96c78a8349f3e40bd5b- Size
- +99 −18 in 3 files
- Review
- pending
- Shared
- 2026-10-28
- Id
e8876071c2c2bffaec94bbf2
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/cmd/blocks.rs crates/engine/src/cmd/blocks/purge.rs crates/engine/tests/wblock_units_handles.rs
@@ -341,6 +341,8 @@ fn run_wblock(s: &mut Session, p: &Value) -> Result<Value> { d.text_styles = src.text_styles.clone(); d.dim_styles = src.dim_styles.clone(); d.blocks = src.blocks.clone();+ // The written drawing keeps the source's units, as AutoCAD's -WBLOCK does.+ d.header.set_i64("INSUNITS", src.header.i64("INSUNITS", 1)); // The base point becomes the written drawing's origin, its insertion base: by default 0,0 for // objects and the block's base point for a block (`base` is then in block coordinates). let base = point_param(p, "base");@@ -368,6 +370,22 @@ fn run_wblock(s: &mut Session, p: &Value) -> Result<Value> { }) .collect() };+ // Keep only the blocks the written objects use (nested references, dimension geometry and+ // multileader content, as PURGE follows them) and the dimension styles' arrowhead blocks, and+ // make new handles start above those of the blocks that stay.+ let mut frontier: Vec<String> = Vec::new();+ for e in &ents {+ purge::block_refs(e, &mut frontier);+ }+ for st in &d.dim_styles {+ frontier.extend([&st.arrow_block, &st.arrow_block1, &st.arrow_block2, &st.leader_arrow_block].into_iter().cloned());+ }+ let used = purge::reachable_blocks(&d, frontier);+ d.blocks.retain(|_, b| used.contains(&b.name.to_ascii_uppercase()));+ let kept: Vec<Handle> = d.blocks.values().flat_map(|b| b.entities.iter().map(|e| e.handle)).collect();+ for h in kept {+ d.bump_handseed(h);+ } for mut e in ents { e.handle = d.new_handle(); d.model.push(e);@@ -6,7 +6,7 @@ use std::collections::{HashSet, VecDeque}; -use cadcraft_doc::{Drawing, EntityKind, Handle, STANDARD_MLINE_STYLE};+use cadcraft_doc::{Drawing, Entity, EntityKind, Handle, STANDARD_MLINE_STYLE}; use serde_json::{Map, Value, json}; use super::super::qselect::wildcard;@@ -117,18 +117,20 @@ fn entities(d: &Drawing) -> impl Iterator<Item = &std::sync::Arc<cadcraft_doc::E d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())).chain(d.blocks.values().flat_map(|b| b.entities.iter())).take(5_000_000) } -/// Upper-case names of blocks reachable from model space, the layouts and dimensions.-fn used_blocks(d: &Drawing) -> HashSet<String> {- let mut used = HashSet::new();- let mut frontier: Vec<String> = Vec::new();- for e in d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())) {- match &e.kind {- EntityKind::Insert(i) => frontier.push(i.block.clone()),- EntityKind::Dimension(dm) => frontier.extend(dm.block.clone()),- EntityKind::MLeader(m) => frontier.extend(m.block.as_ref().map(|b| b.block.clone())),- _ => {}- }+/// Adds the names of the blocks `e` refers to directly (an insert's block, a dimension's geometry+/// block, a multileader's block content) to `out`.+pub(super) fn block_refs(e: &Entity, out: &mut Vec<String>) {+ match &e.kind {+ EntityKind::Insert(i) => out.push(i.block.clone()),+ EntityKind::Dimension(dm) => out.extend(dm.block.clone()),+ EntityKind::MLeader(m) => out.extend(m.block.as_ref().map(|b| b.block.clone())),+ _ => {} }+}++/// Upper-case names of the blocks in `frontier` and of those they refer to, nested.+pub(super) fn reachable_blocks(d: &Drawing, mut frontier: Vec<String>) -> HashSet<String> {+ let mut used = HashSet::new(); let mut guard = 0; while let Some(n) = frontier.pop() { guard += 1;@@ -137,18 +139,22 @@ fn used_blocks(d: &Drawing) -> HashSet<String> { } if let Some(b) = d.block(&n) { for e in b.entities.iter() {- match &e.kind {- EntityKind::Insert(i) => frontier.push(i.block.clone()),- EntityKind::Dimension(dm) => frontier.extend(dm.block.clone()),- EntityKind::MLeader(m) => frontier.extend(m.block.as_ref().map(|b| b.block.clone())),- _ => {}- }+ block_refs(e, &mut frontier); } } } used } +/// Upper-case names of blocks reachable from model space, the layouts and dimensions.+fn used_blocks(d: &Drawing) -> HashSet<String> {+ let mut frontier: Vec<String> = Vec::new();+ for e in d.model.iter().chain(d.layouts.iter().flat_map(|l| l.entities.iter())) {+ block_refs(e, &mut frontier);+ }+ reachable_blocks(d, frontier)+}+ fn is(name: &str, list: &[&str]) -> bool { list.iter().any(|n| n.eq_ignore_ascii_case(name)) }new file mode 100644@@ -0,0 +1,57 @@+//! WBLOCK writes the source's units, only the blocks the written objects use, and handles that+//! never repeat.++use cadcraft_engine::Session;+use cadcraft_engine::cmd::file::{IoHooks, set_io};+use cadcraft_engine::doc::Drawing;+use serde_json::json;++/// Writes "insunits;block names;handles;distinct handles" so the test can read what would be saved.+fn write(d: &Drawing, _path: &str) -> Result<Vec<u8>, String> {+ let mut handles: Vec<u64> = d.model.iter().map(|e| e.handle.0).collect();+ let mut names = Vec::new();+ for b in d.blocks.values() {+ names.push(b.name.clone());+ handles.extend(b.entities.iter().map(|e| e.handle.0));+ }+ let total = handles.len();+ handles.sort_unstable();+ handles.dedup();+ Ok(format!("{};{};{};{}", d.header.i64("INSUNITS", -1), names.join(","), total, handles.len()).into_bytes())+}++fn written(s: &mut Session, p: serde_json::Value) -> String {+ let path = std::env::temp_dir().join(format!("cadcraft-wblock-units-{}.dxf", std::process::id()));+ let mut p = p;+ p["path"] = json!(path.to_string_lossy());+ s.execute("wblock", &p).unwrap();+ let text = std::fs::read_to_string(&path).unwrap();+ let _ = std::fs::remove_file(&path);+ text+}++fn line(s: &mut Session, y: f64) -> String {+ s.execute("line", &json!({ "points": [[0, y], [1, y]] })).unwrap()["handles"][0].as_str().unwrap().to_string()+}++#[test]+fn wblock_keeps_units_drops_unused_blocks_and_repeats_no_handle() {+ set_io(IoHooks { read: |_, _| Err("no reader in this test".into()), write, plot: None });+ let mut s = Session::new();+ s.execute("units", &json!({ "insunits": 6 })).unwrap();+ // Block "Part" is made first, so its line has one of the drawing's lowest handles.+ let p = line(&mut s, 0.0);+ let ins = s.execute("block", &json!({ "name": "Part", "handles": [p], "keep": "convert" })).unwrap()["insert"].as_str().unwrap().to_string();+ let mut picked = vec![ins];+ for i in 1..300 {+ picked.push(line(&mut s, f64::from(i) * 0.01));+ }+ let u = line(&mut s, 5.0);+ s.execute("block", &json!({ "name": "Unused", "handles": [u], "keep": "delete" })).unwrap();++ // 300 objects (the insert and 299 lines) plus Part's line: the source's units, no Unused,+ // and every handle distinct although the new objects are numbered from the start again.+ assert_eq!(written(&mut s, json!({ "handles": picked })), "6;Part;301;301");+ // A block written by name: its objects go to model space, its definition stays behind.+ assert_eq!(written(&mut s, json!({ "name": "Unused" })), "6;;1;1");+}