collective

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

diff --git a/crates/engine/src/cmd/blocks.rs b/crates/engine/src/cmd/blocks.rs--- a/crates/engine/src/cmd/blocks.rs+++ b/crates/engine/src/cmd/blocks.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);diff --git a/crates/engine/src/cmd/blocks/purge.rs b/crates/engine/src/cmd/blocks/purge.rs--- a/crates/engine/src/cmd/blocks/purge.rs+++ b/crates/engine/src/cmd/blocks/purge.rs@@ -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)) }diff --git a/crates/engine/tests/wblock_units_handles.rs b/crates/engine/tests/wblock_units_handles.rsnew file mode 100644--- /dev/null+++ b/crates/engine/tests/wblock_units_handles.rs@@ -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");+}