collective

Browse / github.com/storytold/cadcraft / patch

DXF read: number viewports in file order when every id is unknown (#1463)

DXF read: number viewports in file order when every id is unknown (#1463) ## Cause ZWCAD (2025) writes every paper-space VIEWPORT with id (group 69) `-1`, i.e. "unknown". `dxf_read.rs` did `t.i(69).unwrap_or(0) as u32`, so every viewport came in as id 4294967295 and none as id 1. On save, `paper_views()` found no sheet viewport and wrote a second, CADCraft-made sheet viewport ahead of the real one; DWG readers number a layout's viewports in file order, so the real sheet became viewport 2 (a model viewport showing the whole sheet) and the drawing's extents grew to cover it. ## What changes - A `69` value that is not positive reads as 0 (unknown), never as a huge `

Status
open
Score
0.00
Adopters
0 other operators
Author
assamaral (imported from pull request #2107, open)
Base
26da98e320d06ae22a171601871c6dd7dd74c542
Size
+91 −1 in 5 files
Review
pending
Shared
2026-10-28
Id
c2882a47fb7554e0fdd4c19d

How it went

No other operator has reported using it yet.

Diff

raw · contributors/people.toml crates/io/src/dxf_read.rs crates/io/src/dxf_read/entities.rs crates/io/src/tests.rs docs/file-format-parity.md

diff --git a/contributors/people.toml b/contributors/people.toml--- a/contributors/people.toml+++ b/contributors/people.toml@@ -91,3 +91,6 @@ sync_github_name = false real_name = "Robert Lee" sync_github_name = true +[people.assamaral]+real_name = "assamaral"+sync_github_name = falsediff --git a/crates/io/src/dxf_read.rs b/crates/io/src/dxf_read.rs--- a/crates/io/src/dxf_read.rs+++ b/crates/io/src/dxf_read.rs@@ -120,6 +120,7 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {     let (paper_block_for, first_layout) = setup_layouts(&mut d, &mut layout_objs, &rx);     place_blocks(&mut d, blocks, &mut rx, &paper_block_for, &first_layout);     place_entities(&mut d, entities, &rx, &paper_block_for, &first_layout);+    number_unknown_viewport_ids(&mut d);     apply_sortents(&mut d, &objs.sortents, &rx.brs, &paper_block_for, &first_layout);     objs.apply(&mut d, &rx);     crate::dxf_image::resolve(&secs, &mut d);@@ -137,6 +138,33 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> {     Ok(d) } +/// Viewport ids another program left unknown (ZWCAD writes 69 = -1, read as 0). A layout whose+/// viewports all lack an id gets them numbered in file order, as DWG readers do; when only+/// some lack one and none is 1, the first viewport is the sheet and gets id 1. Either way the+/// sheet keeps id 1, so saving does not add a second sheet viewport ahead of it (#1463).+fn number_unknown_viewport_ids(d: &mut Drawing) {+    for l in d.layouts.iter_mut() {+        let mut vps: Vec<Entity> = l.entities.iter().filter(|e| matches!(&e.kind, EntityKind::Viewport(_))).map(|e| (**e).clone()).collect();+        let id = |e: &Entity| if let EntityKind::Viewport(v) = &e.kind { v.id } else { 0 };+        if vps.iter().all(|e| id(e) == 0) {+            for (i, e) in vps.iter_mut().enumerate() {+                if let EntityKind::Viewport(v) = &mut e.kind {+                    v.id = i as u32 + 1;+                }+            }+        } else if vps.iter().all(|e| id(e) != 1) && vps.first().is_some_and(|e| id(e) == 0) {+            if let Some(EntityKind::Viewport(v)) = vps.first_mut().map(|e| &mut e.kind) {+                v.id = 1;+            }+        } else {+            continue;+        }+        for e in vps {+            l.entities.replace(e);+        }+    }+}+ /// An entity from the groups of a DXF record of type `kind` (without its `0` group), as the /// reader makes it. AutoLISP's `entmake` and `entmod` are built on it. pub fn entity_from_groups(kind: &str, tags: &[Tag]) -> Option<(Common, EntityKind)> {diff --git a/crates/io/src/dxf_read/entities.rs b/crates/io/src/dxf_read/entities.rs--- a/crates/io/src/dxf_read/entities.rs+++ b/crates/io/src/dxf_read/entities.rs@@ -232,7 +232,8 @@ pub(super) fn entity_with_table_budget(kind: &str, tags: &[Tag], table_cells_lef             height: t.fd(41, 1.0),             view_center: Vec2::new(t.fd(12, 0.0), t.fd(22, 0.0)),             view_height: t.fd(45, 1.0),-            id: t.i(69).unwrap_or(0) as u32,+            // ZWCAD writes -1 (unknown): 0 here, numbered in order by the reader (#1463).+            id: t.i(69).filter(|i| *i > 0).unwrap_or(0) as u32,             locked: t.i(90).unwrap_or(0) & 16384 != 0,             frozen_layers: crate::dxf_ext::read_frozen(tags),             layer_colors: crate::dxf_ext::read_layer_colors(tags),diff --git a/crates/io/src/tests.rs b/crates/io/src/tests.rs--- a/crates/io/src/tests.rs+++ b/crates/io/src/tests.rs@@ -604,6 +604,63 @@ fn viewport_frozen_layers_roundtrip() {     assert!(frozen.contains(&(3, Vec::new())), "{frozen:?}"); } +/// `Layout1` with viewports 1 (the sheet), 2 and 3, written to DXF and read back with the id (69)+/// of every viewport `blank` names replaced by -1 (ZWCAD's "unknown"): the ids and widths read.+fn viewport_ids_read_back(blank: impl Fn(&str) -> bool) -> (Drawing, Vec<(u32, f64)>) {+    let mut d = sample();+    let paper = Space::Paper("Layout1".into());+    for (id, w) in [(1, 420.0), (2, 8.0), (3, 4.0)] {+        let vp = Viewport {+            center: Vec3::new(5.0, 4.0, 0.0),+            width: w,+            height: 6.0,+            view_center: Vec2::new(5.0, 4.0),+            view_height: 6.0,+            id,+            ..Viewport::default()+        };+        d.add(&paper, Common::default(), EntityKind::Viewport(vp)).unwrap();+    }+    let text = write_dxf(&d);+    let mut lines: Vec<String> = text.lines().map(String::from).collect();+    for i in (0..lines.len() - 1).step_by(2) {+        if lines[i].trim() == "69" && blank(lines[i + 1].trim()) {+            lines[i + 1] = "-1".into();+        }+    }+    let back = read_dxf(lines.join("\n").as_bytes()).unwrap();+    let ids =+        back.layouts[0].entities.iter().filter_map(|e| if let EntityKind::Viewport(v) = &e.kind { Some((v.id, v.width)) } else { None }).collect();+    (back, ids)+}++/// ZWCAD writes every VIEWPORT with id (69) -1: they are numbered in file order, the first being+/// the paper-space viewport, so saving adds no second sheet viewport (#1463).+#[test]+fn viewport_ids_unknown_are_numbered_in_order() {+    let (back, ids) = viewport_ids_read_back(|_| true);+    assert_eq!(ids, [(1, 420.0), (2, 8.0), (3, 4.0)]);+    assert!(!write_dxf(&back).contains("PAPERVIEW"), "the layout already has its paper-space viewport");+}++/// A layout that mixes known ids with -1 is left alone: the known ids stay, the unknown one reads+/// as 0 (not numbered), as before #1463.+#[test]+fn viewport_ids_mixed_are_kept() {+    let (back, ids) = viewport_ids_read_back(|v| v == "2");+    assert_eq!(ids, [(1, 420.0), (0, 8.0), (3, 4.0)]);+    assert!(!write_dxf(&back).contains("PAPERVIEW"), "viewport 1 is still the sheet");+}++/// Only the sheet's id is unknown: no viewport has id 1, so the first one, the sheet, gets it+/// and saving adds no second sheet (#1463).+#[test]+fn viewport_ids_unknown_sheet_becomes_one() {+    let (back, ids) = viewport_ids_read_back(|v| v == "1");+    assert_eq!(ids, [(1, 420.0), (2, 8.0), (3, 4.0)]);+    assert!(!write_dxf(&back).contains("PAPERVIEW"), "the first viewport is the sheet");+}+ /// A layout with viewport 2 frozen on `Walls` and `A B` (both real layers), and its written DXF as /// (code, value) pairs. fn frozen_vp_dxf() -> (Drawing, Vec<(String, String)>) {diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md--- a/docs/file-format-parity.md+++ b/docs/file-format-parity.md@@ -141,6 +141,7 @@ our output in AutoCAD black-box. | Date | Change | Summary | |---|---|---| | 2026-10-25 | minor | LAYOUT SAveas (`layout.saveas`) writes a layout as a drawing template: its page setup and paper space objects, no model space objects or other layouts, and only the named objects they use |+| 2026-10-24 | minor | DXF read: a layout whose VIEWPORTs all carry id (69) -1 (ZWCAD writes no ids) gets them numbered in file order, the first being the paper-space viewport, so a save no longer adds a second sheet viewport that turns the real one into a model viewport and inflates the drawing extents (#1463) | | 2026-10-22 | minor | DXF/DWG save: angular, diametric and radial dimensions get standard DIMASSOC objects (one centre reference to the circle for radial/diametric, the definition points for angular), so AutoCAD follows their geometry; the standard objects of those kinds are read back too, without CADCraft's xdata (#1651) | | 2026-10-19 | minor | DXF/DWG save: angular dimension block text honors DIMDSEP, including per-dimension overrides (#1646) | | 2026-10-19 | minor | A file's own arrowhead blocks with standard names (AutoCAD's `_Dot`, `_DotSmall`), named only by a dimension style, are kept and saved as they were instead of being replaced by CADCraft's solids; PURGE keeps arrowhead blocks dimension styles name |