Browse / github.com/storytold/cadcraft / patch
Keep LAYOUT limits and paper image origin on DXF round trip
LAYOUT object written with fixed limits (0,0)–(12,9) and zero paper image origin Split out of #1463 (see https://github.com/storytold/cadcraft/issues/1463#issuecomment-6384594279), as @ffishman suggested. ## What happens The `LAYOUT` object written by `crates/io/src/dxf_write.rs` (the `layout_obj` closure) has fixed values for every drawing: - `AcDbLayout` groups 10/11 (LIMMIN/LIMMAX of the layout): `(0, 0)` – `(12, 9)` - `AcDbPlotSettings` groups 148/149 (paper image origin): `0, 0` The file's own values are not read back or kept. Measured on a ZWCAD 2025 drawing (private, ISO A3 layouts at plot scale 49.6): the original `LAYOUT` had LIMMIN/LIMMAX `(1.038, 0.694)` –
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Silvertree2010 (imported from pull request #2132, open)
- Base
e86d895ba1086a7efc2a83048348f89098282d08- Size
- +174 −13 in 6 files
- Review
- pending
- Shared
- 2026-10-28
- Id
e4e9dda7d57060c12c154043
How it went
No other operator has reported using it yet.
Diff
raw · crates/doc/src/tables.rs crates/engine/src/cmd/layout.rs crates/io/src/dxf_read.rs crates/io/src/dxf_write.rs crates/io/tests/it/layout_limits_roundtrip.rs crates/io/tests/it/main.rs
@@ -787,6 +787,10 @@ pub struct PageSetup { /// The layout's paper units are millimetres (plot paper units 1 in DXF) whatever the /// drawing's MEASUREMENT says; otherwise paper units follow MEASUREMENT (#1058). pub paper_mm: bool,+ /// Paper image origin (DXF plot settings groups 148/149) as read from a file; `None` for a+ /// page setup CADCraft created (written as 0,0).+ #[serde(default)]+ pub paper_image_origin: Option<Vec2>, } impl Default for PageSetup {@@ -808,6 +812,7 @@ impl Default for PageSetup { show_plot_styles: false, plot_transparency: false, paper_mm: false,+ paper_image_origin: None, } } }@@ -823,11 +828,15 @@ pub struct Layout { /// Saved paper-space view (centre, height). #[serde(default)] pub view: Option<(Vec2, f64)>,+ /// The layout's limits (LIMMIN, LIMMAX; DXF AcDbLayout groups 10/11) as read from a file;+ /// `None` for a layout CADCraft created (written as (0,0)-(12,9)).+ #[serde(default)]+ pub limits: Option<(Vec2, Vec2)>, } impl Layout { pub fn new(name: &str, tab_order: u32) -> Self {- Layout { name: name.into(), tab_order, page: PageSetup::default(), entities: EntityStore::new(), view: None }+ Layout { name: name.into(), tab_order, page: PageSetup::default(), entities: EntityStore::new(), view: None, limits: None } } } @@ -309,7 +309,7 @@ fn run_copy(s: &mut Session, p: &Value) -> Result<Value> { let h = d.new_handle(); store.push(Entity { handle: h, ..Entity::clone(e) }); }- d.layouts.push(Layout { name: to.clone(), tab_order: order, page: src.page, entities: store, view: src.view });+ d.layouts.push(Layout { name: to.clone(), tab_order: order, page: src.page, entities: store, view: src.view, limits: src.limits }); Ok(json!({ "name": to })) } @@ -1701,7 +1701,7 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> { d.header.set_i64("INSUNITS", 0); let mut rx = Rx { pstyles: plot_style_names(&secs), ..Rx::default() }; let mut blocks: Vec<(String, Vec3, String, Vec<(Option<String>, bool, Entity)>, i64)> = Vec::new();- let mut layout_objs: Vec<(String, u32, String, PageSetup)> = Vec::new();+ let mut layout_objs: Vec<(String, u32, String, PageSetup, Option<(Vec2, Vec2)>)> = Vec::new(); let mut entities = Vec::new(); let mut objs = Objects::default(); // Every handle another program's file already uses is taken before anything gets a new one:@@ -1774,7 +1774,17 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> { // LAYOUT has two 330s: owner dict and the block record; take the last. let br = tg.iter().rfind(|x| x.code == 330).map(Tag::str).unwrap_or_default(); let plot = T(tg.get(..start).unwrap_or(&[]));- layout_objs.push((t.s(1).unwrap_or_default(), t.i(71).unwrap_or(0) as u32, br.to_ascii_uppercase(), page_setup(&plot)));+ // Limits (10/11); kept only when all four values are finite and differ+ // from what CADCraft writes for a layout it created.+ let pt = |c: i32| Some(Vec2::new(t.f(c)?, t.f(c + 10)?)).filter(|p| p.x.is_finite() && p.y.is_finite());+ let limits = pt(10).zip(pt(11)).filter(|l| *l != (Vec2::ZERO, Vec2::new(12.0, 9.0)));+ layout_objs.push((+ t.s(1).unwrap_or_default(),+ t.i(71).unwrap_or(0) as u32,+ br.to_ascii_uppercase(),+ page_setup(&plot),+ limits,+ )); } } }@@ -1787,11 +1797,11 @@ fn read_budgeted(bytes: &[u8], mut table_cells_left: usize) -> Result<Drawing> { // Layouts. let mut paper_block_for: HashMap<String, String> = HashMap::new(); layout_objs.sort_by_key(|l| l.1);- for (name, order, br, page) in &layout_objs {+ for (name, order, br, page, limits) in &layout_objs { if name.eq_ignore_ascii_case("Model") { continue; }- d.layouts.push(Layout { page: page.clone(), ..Layout::new(name, *order) });+ d.layouts.push(Layout { page: page.clone(), limits: *limits, ..Layout::new(name, *order) }); if let Some(bname) = rx.brs.get(br) { paper_block_for.insert(bname.to_ascii_uppercase(), name.clone()); }@@ -2366,6 +2376,9 @@ fn page_setup(t: &T) -> PageSetup { .into(); p.plot_window = [t.fd(48, 0.0), t.fd(49, 0.0), t.fd(140, 0.0), t.fd(141, 0.0)].map(|v| if v.is_finite() { v } else { 0.0 }); p.plot_style_table = t.s(7).unwrap_or_default();+ // Paper image origin (148/149); kept only when both values are finite and not 0,0 (what+ // CADCraft writes for a page setup it created).+ p.paper_image_origin = t.f(148).zip(t.f(149)).map(|(x, y)| Vec2::new(x, y)).filter(|o| o.x.is_finite() && o.y.is_finite() && *o != Vec2::ZERO); p } @@ -2612,7 +2612,15 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> { w.s(3, &l.name); w.s(350, layout_handles.get(i).cloned().unwrap_or_default()); }- let layout_obj = |w: &mut W, h: &str, name: &str, tab: i64, brh: &str, flags: i64, page: &PageSetup, view: Option<(Vec2, f64)>| {+ let layout_obj = |w: &mut W,+ h: &str,+ name: &str,+ tab: i64,+ brh: &str,+ flags: i64,+ page: &PageSetup,+ view: Option<(Vec2, f64)>,+ limits: Option<(Vec2, Vec2)>| { // Plot scale: a standard scale when fitting or 1:1, otherwise custom (142 paper units // per 143 drawing units). let scale = if page.scale.is_finite() && page.scale > 0.0 { page.scale } else { 1.0 };@@ -2665,14 +2673,19 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> { w.s(7, &page.plot_style_table); w.i(75, if page.scale_to_fit { 0 } else { 16 }); w.f(147, scale);- w.f(148, 0.0);- w.f(149, 0.0);+ // Paper image origin and limits: the file's own values, else the defaults (a layout+ // CADCraft created, or a non-finite value).+ let finite = |p: &Vec2| p.x.is_finite() && p.y.is_finite();+ let origin = page.paper_image_origin.filter(finite).unwrap_or(Vec2::ZERO);+ w.f(148, origin.x);+ w.f(149, origin.y); w.s(100, "AcDbLayout"); w.s(1, name); w.i(70, flags); w.i(71, tab);- w.p2(10, Vec2::ZERO);- w.p2(11, Vec2::new(12.0, 9.0));+ let (lim_min, lim_max) = limits.filter(|(a, b)| finite(a) && finite(b)).unwrap_or((Vec2::ZERO, Vec2::new(12.0, 9.0)));+ w.p2(10, lim_min);+ w.p2(11, lim_max); w.p(12, Vec3::ZERO); w.p(14, Vec3::ZERO); w.p(15, Vec3::ZERO);@@ -2697,10 +2710,10 @@ fn tags_for(d: &Drawing, version: Version, target: Version) -> Vec<Tag> { // 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);+ layout_obj(&mut w, &model_layout, "Model", 0, &ms_br, 1, &model_page, None, 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);+ layout_obj(&mut w, &h, &l.name, i64::from(l.tab_order.max(1)), brh, 1, &l.page, l.view, l.limits); } // Raster image definitions. w.t.extend(crate::dxf_image::objects(&cx.images, &root_dict));new file mode 100644@@ -0,0 +1,125 @@+//! A layout's limits (AcDbLayout groups 10/11) and paper image origin (AcDbPlotSettings groups+//! 148/149) survive a DXF round trip (#2109). They place the sheet in paper space; with fixed+//! values another program drew the sheet away from the entities and shifted a "Layout" plot.+//! Layouts CADCraft creates keep (0,0)-(12,9) and origin 0,0.++use cadcraft_doc::*;+use cadcraft_geom::Vec2;++/// DXF text with groups of the "Layout1" LAYOUT record set to new values: (code, value,+/// whether the group comes after the record's `AcDbLayout` marker).+fn set_paper_layout(text: &str, values: &[(&str, &str, bool)]) -> String {+ let lines: Vec<&str> = text.lines().collect();+ let mut out: Vec<String> = Vec::with_capacity(lines.len());+ let mut i = 0;+ while i + 1 < lines.len() {+ let (code, value) = (lines[i].trim(), lines[i + 1].trim());+ if code == "0" && value == "LAYOUT" {+ // Collect the record.+ let mut j = i + 2;+ while j + 1 < lines.len() && lines[j].trim() != "0" {+ j += 2;+ }+ let mut rec: Vec<(String, String)> = (i..j).step_by(2).map(|k| (lines[k].to_string(), lines[k + 1].to_string())).collect();+ let at = rec.iter().position(|(c, v)| c.trim() == "100" && v.trim() == "AcDbLayout").unwrap();+ let name = rec.iter().skip(at).find(|(c, _)| c.trim() == "1").map(|(_, v)| v.trim().to_string()).unwrap();+ if name == "Layout1" {+ for (c, v, after) in values {+ let k = rec.iter().enumerate().position(|(k, (rc, _))| rc.trim() == *c && (k > at) == *after).unwrap();+ rec[k].1 = v.to_string();+ }+ }+ for (c, v) in rec {+ out.push(c);+ out.push(v);+ }+ i = j;+ } else {+ out.push(lines[i].to_string());+ out.push(lines[i + 1].to_string());+ i += 2;+ }+ }+ out.extend(lines.iter().skip(i).map(|l| l.to_string()));+ out.join("\n") + "\n"+}++/// Limits (10, 20, 11, 21) and paper image origin (148, 149) of every paper-space LAYOUT.+fn limits(text: &str) -> Vec<[f64; 6]> {+ let lines: Vec<&str> = text.lines().map(str::trim).collect();+ let mut recs: Vec<Vec<(String, String)>> = Vec::new();+ let mut inside = false;+ for pair in lines.chunks(2) {+ let [code, value] = pair else { break };+ if *code == "0" {+ inside = *value == "LAYOUT";+ if inside {+ recs.push(Vec::new());+ }+ } else if inside && let Some(rec) = recs.last_mut() {+ rec.push((code.to_string(), value.to_string()));+ }+ }+ recs.iter()+ .filter_map(|rec| {+ let at = rec.iter().position(|(c, v)| c == "100" && v == "AcDbLayout")?;+ let name = rec.iter().skip(at).find(|(c, _)| c == "1").map(|(_, v)| v.as_str())?;+ if name == "Model" {+ return None;+ }+ let after = |code: &str| rec.iter().skip(at).find(|(c, _)| c == code).and_then(|(_, v)| v.parse().ok()).unwrap_or(f64::NAN);+ let before = |code: &str| rec.iter().take(at).find(|(c, _)| c == code).and_then(|(_, v)| v.parse().ok()).unwrap_or(f64::NAN);+ Some([after("10"), after("20"), after("11"), after("21"), before("148"), before("149")])+ })+ .collect()+}++/// What CADCraft writes for a layout it created.+const DEFAULT: [f64; 6] = [0.0, 0.0, 12.0, 9.0, 0.0, 0.0];++#[test]+fn layout_limits_and_paper_image_origin_survive_a_round_trip() {+ let src = cadcraft_io::write_dxf(&Drawing::new_metric());+ let edited = set_paper_layout(+ &src,+ &[+ ("10", "1.038", true),+ ("20", "0.694", true),+ ("11", "9.506", true),+ ("21", "6.681", true),+ ("148", "-1.054", false),+ ("149", "-0.71", false),+ ],+ );+ let ours = [1.038, 0.694, 9.506, 6.681, -1.054, -0.71];+ assert_eq!(limits(&edited), vec![ours, DEFAULT]);+ let d = cadcraft_io::read_dxf(edited.as_bytes()).unwrap();+ let out = cadcraft_io::write_dxf(&d);+ // Layout1 keeps the file's values; Layout2 had the defaults and keeps them.+ assert_eq!(limits(&out), vec![ours, DEFAULT]);+}++#[test]+fn new_layouts_keep_the_default_limits_and_origin() {+ let out = cadcraft_io::write_dxf(&Drawing::new_metric());+ assert_eq!(limits(&out), vec![DEFAULT, DEFAULT]);+}++#[test]+fn non_finite_limits_and_origin_fall_back_to_the_defaults() {+ let src = cadcraft_io::write_dxf(&Drawing::new_metric());+ let edited = set_paper_layout(&src, &[("10", "NaN", true), ("148", "NaN", false)]);+ let d = cadcraft_io::read_dxf(edited.as_bytes()).unwrap();+ let out = cadcraft_io::write_dxf(&d);+ assert_eq!(limits(&out), vec![DEFAULT, DEFAULT]);+}++#[test]+fn non_finite_limits_and_origin_in_memory_write_the_defaults() {+ let mut d = Drawing::new_metric();+ for l in &mut d.layouts {+ l.limits = Some((Vec2::new(f64::NAN, 0.0), Vec2::new(12.0, f64::INFINITY)));+ l.page.paper_image_origin = Some(Vec2::new(f64::NAN, 1.0));+ }+ assert_eq!(limits(&cadcraft_io::write_dxf(&d)), vec![DEFAULT, DEFAULT]);+}@@ -29,6 +29,7 @@ mod groups_roundtrip; mod hatch_assoc_roundtrip; mod hatch_pattern_def; mod helix_roundtrip;+mod layout_limits_roundtrip; mod layout_paper_units; mod layout_plot_window; #[cfg(not(target_arch = "wasm32"))]