From 162d5c682063536d5070a04b1abdc66c304076e6 Mon Sep 17 00:00:00 2001 From: aldozar Date: Mon, 19 Oct 2026 00:28:58 -0600 Subject: [PATCH] DWG: entities of a second layout stay on it in AutoCAD (#1057) (#1080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit acadrust writes every paper-space entity with entity mode 1 ("paper space"), including those of *Paper_Space0, *Paper_Space1… and relies on each block's entity list; AutoCAD reads mode 1 as the *Paper_Space block's, so what was drawn on a second layout (and its viewports, in drawings from AutoCAD with several layouts) landed on the first one, and the second stayed empty. The DWG bridge now writes those entities owned by their block (entity mode 0 with the owner handle), as the public DWG specification describes; the first layout's entities stay mode 1. The io tests read CADCraft's DWG back with acadrust (a dev-dependency, the same version as cadcraft-dwg's) to check the modes as written. Co-authored-by: Claude Opus 5.5 --- Cargo.lock | 1 + crates/dwg/src/lib.rs | 26 +++++++++++++++ crates/io/Cargo.toml | 2 ++ crates/io/tests/dwg_layout_owner.rs | 52 +++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+) create mode 100644 crates/io/tests/dwg_layout_owner.rs diff --git a/Cargo.lock b/Cargo.lock index 043ca01e..3eb9cacc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -663,6 +663,7 @@ dependencies = [ name = "cadcraft-io" version = "0.9.0" dependencies = [ + "acadrust", "cadcraft-color", "cadcraft-doc", "cadcraft-dwg", diff --git a/crates/dwg/src/lib.rs b/crates/dwg/src/lib.rs index 7600d63e..c5d17cd0 100644 --- a/crates/dwg/src/lib.rs +++ b/crates/dwg/src/lib.rs @@ -423,6 +423,7 @@ mod native { None if doc.version < MIN_DWG_VERSION => doc.version = MIN_DWG_VERSION, None => {} } + other_layout_owners(&mut doc); table_style_rows(&mut doc, true); drop_unwritable(&mut doc); current_dimstyle(&mut doc); @@ -430,6 +431,31 @@ mod native { .map_err(|_| "the DWG writer failed on this drawing".to_string())? .map_err(|e| format!("DWG write: {e}")) } + + /// Entities of a layout other than the one in `*Paper_Space` are written owned by their block + /// (entity mode 0, with its handle). acadrust writes every paper-space entity with mode 1, + /// "paper space", which AutoCAD reads as the `*Paper_Space` block's: everything drawn on a + /// second layout landed on the first one, and the second stayed empty (#1057). + fn other_layout_owners(doc: &mut acadrust::CadDocument) { + let others: std::collections::HashSet = doc + .block_records + .iter() + .filter(|br| { + let n = br.name.to_ascii_uppercase(); + n.len() > 12 && n.starts_with("*PAPER_SPACE") && n[12..].bytes().all(|b| b.is_ascii_digit()) + }) + .map(|br| br.handle) + .collect(); + if others.is_empty() { + return; + } + for e in doc.entities_mut() { + let c = e.common_mut(); + if others.contains(&c.owner_handle) { + c.entity_mode = Some(0); + } + } + } } #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/io/Cargo.toml b/crates/io/Cargo.toml index 151e1ec9..84adfec4 100644 --- a/crates/io/Cargo.toml +++ b/crates/io/Cargo.toml @@ -23,4 +23,6 @@ miniz_oxide = "0.8" quick-xml = { workspace = true } [dev-dependencies] +# Reads CADCraft's DWG output as written (tests only; the same version as cadcraft-dwg's). +acadrust = "0.6.4" tiny-skia = { workspace = true } diff --git a/crates/io/tests/dwg_layout_owner.rs b/crates/io/tests/dwg_layout_owner.rs new file mode 100644 index 00000000..45de12cb --- /dev/null +++ b/crates/io/tests/dwg_layout_owner.rs @@ -0,0 +1,52 @@ +//! #1057: in a DWG, entities of a layout other than the first (`*Paper_Space`) are written owned +//! by their own block (entity mode 0 and its handle). Written as "paper space" (mode 1), AutoCAD +//! put them on the first layout and left theirs empty. + +use std::io::Cursor; + +use cadcraft_doc::{Circle, Common, Drawing, EntityKind, Layout, Line, Space}; +use cadcraft_geom::Vec3; +use cadcraft_io::{SaveOptions, Version, read, write_with}; + +#[test] +fn entities_of_a_second_layout_are_owned_by_its_block() { + let mut d = Drawing::new_metric(); + let order = d.layouts.iter().map(|l| l.tab_order).max().unwrap_or(0) + 1; + d.layouts.push(Layout::new("SHEET_A", order)); + let line = |a: f64| EntityKind::Line(Line { a: Vec3::new(a, 0.0, 0.0), b: Vec3::new(a + 10.0, 10.0, 0.0) }); + d.add(&Space::Model, Common::default(), EntityKind::Circle(Circle { center: Vec3::ZERO, radius: 5.0 })).unwrap(); + let first = d.layouts.iter().min_by_key(|l| l.tab_order).unwrap().name.clone(); + d.add(&Space::Paper(first.clone()), Common::default(), line(0.0)).unwrap(); + d.add(&Space::Paper("SHEET_A".into()), Common::default(), line(20.0)).unwrap(); + d.add(&Space::Paper("SHEET_A".into()), Common::default(), line(40.0)).unwrap(); + let dwg = write_with(&d, "case.dwg", &SaveOptions { version: Some(Version::R2018), binary: false }).unwrap(); + + // As written: entity mode and owner of each entity. + let doc = acadrust::DwgReader::from_stream(Cursor::new(dwg.clone())).read().unwrap(); + let block_of = |h: acadrust::Handle| doc.block_records.iter().find(|b| b.handle == h).map(|b| b.name.to_ascii_uppercase()); + let sheet_a = doc + .objects + .iter() + .find_map(|(_, o)| match o { + acadrust::objects::ObjectType::Layout(l) if l.name == "SHEET_A" => Some(l.block_record), + _ => None, + }) + .unwrap(); + let mut on_sheet_a = 0; + for e in doc.entities() { + let c = e.common(); + let block = block_of(c.owner_handle).unwrap_or_default(); + if c.owner_handle == sheet_a { + on_sheet_a += 1; + assert_eq!(c.entity_mode, Some(0), "an entity of SHEET_A ({block}) is owned by its block"); + } else if block == "*PAPER_SPACE" { + assert_eq!(c.entity_mode, Some(1), "the first layout's entities stay paper space"); + } + } + assert_eq!(on_sheet_a, 2); + + // Read back, each layout has its own. + let back = read(&dwg, "case.dwg").unwrap(); + let count = |n: &str| back.layouts.iter().find(|l| l.name == n).map_or(0, |l| l.entities.len()); + assert_eq!((count(&first), count("SHEET_A"), back.model.len()), (1, 2, 1)); +}