diff --git a/crates/io/src/dxf_write.rs b/crates/io/src/dxf_write.rs index 24fbaecd..70a3d57c 100644 --- a/crates/io/src/dxf_write.rs +++ b/crates/io/src/dxf_write.rs @@ -2299,7 +2299,12 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec { w.s(1002, "}"); } }; - layout_obj(&mut w, &model_layout, "Model", 0, &ms_br, 1, &PageSetup::default(), None); + // Model space has no page setup of its own: write what PLOT does for it, the drawing extents + // fitted to the paper (plot type 1, scaled to fit). The default page's "layout" area is + // plot type 5, "Layout information" (DXF Reference, PLOTSETTINGS group 74), which means + // nothing for model space: another program's Plot dialog then shows no plot area or scale. + let model_page = PageSetup { plot_area: "extents".into(), scale_to_fit: true, ..PageSetup::default() }; + layout_obj(&mut w, &model_layout, "Model", 0, &ms_br, 1, &model_page, None); for (i, (_, brh, l)) in ps_brs.iter().enumerate() { let h = layout_handles.get(i).cloned().unwrap_or_default(); layout_obj(&mut w, &h, &l.name, i64::from(l.tab_order.max(1)), brh, 1, &l.page, l.view); diff --git a/crates/io/tests/model_layout_plot_type.rs b/crates/io/tests/model_layout_plot_type.rs new file mode 100644 index 00000000..3de6380e --- /dev/null +++ b/crates/io/tests/model_layout_plot_type.rs @@ -0,0 +1,68 @@ +//! The Model layout's plot settings say what CADCraft plots for model space: the drawing extents +//! fitted to the paper. Plot type 5 ("Layout information", DXF Reference, PLOTSETTINGS group 74) +//! means nothing for model space; another program's Plot dialog then showed no plot area and no +//! scale for Model. +#![cfg(not(target_arch = "wasm32"))] + +use cadcraft_doc::*; +use cadcraft_geom::Vec3; + +/// The (code, value) pairs of every `kind` record in DXF text. +fn records(text: &str, kind: &str) -> Vec> { + let lines: Vec<&str> = text.lines().map(str::trim).collect(); + let mut out: Vec> = Vec::new(); + let mut inside = false; + for pair in lines.chunks(2) { + let [code, value] = pair else { break }; + if *code == "0" { + inside = *value == kind; + if inside { + out.push(Vec::new()); + } + } else if inside && let Some(rec) = out.last_mut() { + rec.push((code.to_string(), value.to_string())); + } + } + out +} + +/// A LAYOUT record's name (the code 1 after the `AcDbLayout` marker), plot type (74) and +/// standard scale type (75). +fn layout_plot(rec: &[(String, String)]) -> (String, Option, Option) { + let at = rec.iter().position(|(c, v)| c == "100" && v == "AcDbLayout").unwrap_or(rec.len()); + let name = rec.iter().skip(at).find(|(c, _)| c == "1").map(|(_, v)| v.clone()).unwrap_or_default(); + let int = |code: &str| rec.iter().take(at).find(|(c, _)| c == code).and_then(|(_, v)| v.trim().parse().ok()); + (name, int("74"), int("75")) +} + +fn plots(text: &str) -> Vec<(String, Option, Option)> { + records(text, "LAYOUT").iter().map(|r| layout_plot(r)).collect() +} + +fn drawing() -> Drawing { + let mut d = Drawing::new_metric(); + d.add(&Space::Model, Common::default(), EntityKind::Line(Line { a: Vec3::ZERO, b: Vec3::new(100.0, 50.0, 0.0) })).unwrap(); + d +} + +#[test] +fn model_layout_plots_extents_fitted_and_layouts_their_sheet() { + let p = plots(&cadcraft_io::write_dxf(&drawing())); + let model = p.iter().find(|(n, _, _)| n == "Model").unwrap(); + assert_eq!((model.1, model.2), (Some(1), Some(0)), "Model: drawing extents, scaled to fit"); + // Paper-space layouts keep "Layout information" at 1:1. + let layouts: Vec<_> = p.iter().filter(|(n, _, _)| n != "Model").collect(); + assert!(!layouts.is_empty()); + assert!(layouts.iter().all(|(_, t, s)| (*t, *s) == (Some(5), Some(16))), "{layouts:?}"); +} + +#[test] +fn model_layout_plot_type_survives_the_dwg_bridge() { + let dwg = cadcraft_io::write(&drawing(), "x.dwg").unwrap(); + // What the DWG holds, as the bridge's own DXF. + let dxf = String::from_utf8_lossy(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).to_string(); + let p = plots(&dxf); + let model = p.iter().find(|(n, _, _)| n.eq_ignore_ascii_case("Model")).unwrap(); + assert_eq!(model.1, Some(1), "{p:?}"); + assert_ne!(p.iter().find(|(n, _, _)| !n.eq_ignore_ascii_case("Model")).and_then(|l| l.1), None); +} diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index 0a4158b0..51426a51 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File-format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (code pages, #505; native MULTILEADER, #459; foreign data kept, #528; PDF text, layers and sheets, #567, #571; templates and user files, #552; plotter devices, #564) · **Target:** Autodesk AutoCAD 2027 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (the Model layout's plot type is written as extents, not "Layout information") · **Target:** Autodesk AutoCAD 2027 Every format AutoCAD 2027 reads or writes, with CADCraft's support, fidelity and tests. Work items are in [gaps.md](gaps.md#file-formats); the weighted score feeds @@ -134,6 +134,7 @@ our output in AutoCAD black-box. | Date | Change | Summary | |---|---|---| +| 2026-10-12 | minor | DXF/DWG save: the Model layout's plot settings are written as CADCraft plots model space, drawing extents scaled to fit (PLOTSETTINGS 74 = 1, 75 = 0), instead of plot type 5 "Layout information", which other programs' Plot dialogs can't use for model space | | 2026-10-11 | minor | Rows refreshed for merged work: code pages and `\M+` (#505), MULTILEADER (#459), IMAGE, MLINE/HELIX/REGION/TOLERANCE, VPORT/VIEW/UCS tables, modelled and raw objects, foreign data (#528), DWT templates and user `.lin`/`.pat`/`.pgp` files (#552), PDF output (#567, #571), plotter devices (#564), data extraction (#433, #534); weighted percentages not re-measured | | 2026-10-11 | minor | DXF (ASCII and binary) and DWG saved at 2000–2018, 2018 by default (#421) | | 2026-10-10 | minor | WIPEOUT and gradients written since #147/#162; other save losses remain (#201, #204, #208, #209, #213, #223) |