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
@@ -91,3 +91,6 @@ sync_github_name = false real_name = "Robert Lee" sync_github_name = true +[people.assamaral]+real_name = "assamaral"+sync_github_name = false@@ -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)> {@@ -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),@@ -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)>) {@@ -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 |