Merge branch 'stk-2028' into stack/s1

This commit is contained in:
Tanish Manku
2026-11-05 17:23:11 -07:00
6 changed files with 159 additions and 5 deletions
+8
View File
@@ -799,6 +799,13 @@ 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 size (DXF groups 44, 45) and plot rotation (group 73) as the file stored them.
/// Saving writes them back while they still describe `width_mm`, `height_mm` and
/// `landscape`, so each program's convention for media defined wider than tall survives a
/// round trip: AutoCAD stores a portrait size and a quarter turn, ZWCAD the media's own size
/// and no turn (#1463). `None` for page setups made in CADCraft.
#[serde(skip)]
pub file_paper: Option<(f64, f64, i64)>,
/// The paper image origin (DXF groups 148/149, in plot paper units) as the file had it, so a
/// save keeps where the sheet sits; `None` for a page CADCraft made, written as 0,0 (#2109).
#[serde(skip_serializing_if = "Option::is_none")]
@@ -824,6 +831,7 @@ impl Default for PageSetup {
show_plot_styles: false,
plot_transparency: false,
paper_mm: false,
file_paper: None,
image_origin: None,
}
}
+1
View File
@@ -252,6 +252,7 @@ pub(super) fn page_setup(t: &T) -> PageSetup {
let rotated = matches!(t.i(73), Some(1 | 3));
if w > 0.0 && h > 0.0 {
p.landscape = rotated != (w > h);
p.file_paper = Some((w, h, t.i(73).unwrap_or(0)));
} else if t.i(73).is_some() {
p.landscape = rotated;
}
+40 -3
View File
@@ -425,13 +425,14 @@ fn layout_obj(w: &mut W, context: LayoutWriteContext<'_>, data: LayoutWriteData<
w.s(2, if device.is_empty() || device.eq_ignore_ascii_case("None") { "none_device" } else { device });
w.s(4, page.paper.replace(' ', "_"));
w.s(6, "");
let (paper_w, paper_h, rotation) = plot_paper(page);
for (c, v) in [
(40, page.margins_mm[0]),
(41, page.margins_mm[1]),
(42, page.margins_mm[2]),
(43, page.margins_mm[3]),
(44, page.width_mm),
(45, page.height_mm),
(44, paper_w),
(45, paper_h),
(46, 0.0),
(47, 0.0),
(48, page.plot_window[0]),
@@ -446,7 +447,7 @@ fn layout_obj(w: &mut W, context: LayoutWriteContext<'_>, data: LayoutWriteData<
w.i(70, plot_flags);
// Plot paper units: 1 millimetres, 0 inches (#1058).
w.i(72, i64::from(cadcraft_render::paper::page_unit_mm(d, page) == 1.0));
w.i(73, i64::from(page.landscape));
w.i(73, rotation);
w.i(74, plot_type);
w.s(7, &page.plot_style_table);
w.i(75, if page.scale_to_fit { 0 } else { 16 });
@@ -1150,6 +1151,42 @@ pub(super) fn tags_for(d: &Drawing, version: Version, target: Version, frozen_34
w.t
}
/// Paper size (groups 44, 45) and plot rotation (group 73) for a LAYOUT object. Page setups
/// keep a portrait size and a landscape flag. The values read from the file are written back
/// as they were while they still describe that size and orientation, whatever convention the
/// program that wrote them follows. Otherwise a media whose name makes it wider than tall,
/// such as `ISO_full_bleed_A3_(420.00_x_297.00_MM)`, gets its size in that order and the
/// rotation relative to it, so readers that take the media's size from its name agree with
/// groups 44 and 45 (#1463). Other media are written as before.
fn plot_paper(page: &PageSetup) -> (f64, f64, i64) {
if let Some((w, h, rotation)) = page.file_paper {
let same = |a: f64, b: f64| (a - b).abs() < 1e-6;
let landscape = matches!(rotation, 1 | 3) != (w > h);
if same(w.min(h), page.width_mm) && same(w.max(h), page.height_mm) && landscape == page.landscape {
return (w, h, rotation);
}
}
if media_is_landscape(&page.paper) {
let (short, long) = (page.width_mm.min(page.height_mm), page.width_mm.max(page.height_mm));
(long, short, i64::from(!page.landscape))
} else {
(page.width_mm, page.height_mm, i64::from(page.landscape))
}
}
/// Whether a media name ends in a size wider than tall, like `… (420.00 x 297.00 MM)`.
fn media_is_landscape(paper: &str) -> bool {
let name = paper.replace('_', " ");
let Some(open) = name.rfind('(') else { return false };
let mut sides = name[open + 1..].splitn(2, ['x', 'X', '×']);
let num = |s: Option<&str>| {
let s = s?.trim_start();
let end = s.find(|c: char| !(c.is_ascii_digit() || c == '.')).unwrap_or(s.len());
s[..end].parse::<f64>().ok()
};
matches!((num(sides.next()), num(sides.next())), (Some(w), Some(h)) if w > h)
}
/// DXF Reference group 91 of CLASS records, the instance count, which came with the 2004 format.
/// From 2004 every CLASS record gets its actual count, counted after `dxf_raw::finish` restores
/// foreign classes and objects (AutoCAD rejects a 2004+ file without it, #635). An R2000 file has
+104
View File
@@ -743,6 +743,110 @@ fn dxf_roundtrips_page_setup() {
assert_eq!(p.paper, a3.name);
}
/// Groups 44, 45 and 73 of a layout's AcDbPlotSettings in DXF text.
fn plot_paper_groups(dxf: &str, layout: &str) -> (f64, f64, i64) {
let lines: Vec<&str> = dxf.lines().map(str::trim).collect();
let pairs: Vec<(&str, &str)> = lines.chunks(2).filter(|c| c.len() == 2).map(|c| (c[0], c[1])).collect();
let name = pairs.windows(2).position(|w| w[0] == ("100", "AcDbLayout") && w[1] == ("1", layout)).unwrap_or_else(|| panic!("no LAYOUT {layout}"));
let start = pairs[..name].iter().rposition(|p| *p == ("100", "AcDbPlotSettings")).unwrap();
let get = |code: &str| pairs[start..name].iter().find(|p| p.0 == code).map(|p| p.1).unwrap();
(get("44").parse().unwrap(), get("45").parse().unwrap(), get("73").parse().unwrap())
}
#[test]
fn landscape_media_keeps_its_size_order_and_rotation() {
// A media defined wider than tall, as ZWCAD and AutoCAD name it (#1463).
let media = "ISO full bleed A3 (420.00 x 297.00 MM)";
for (landscape, rotation) in [(true, 0), (false, 1)] {
let mut d = sample();
{
let p = &mut d.layouts[0].page;
p.paper = media.into();
p.width_mm = 297.0;
p.height_mm = 420.0;
p.landscape = landscape;
}
let dxf = write_dxf(&d);
assert_eq!(plot_paper_groups(&dxf, "Layout1"), (420.0, 297.0, rotation), "DXF, landscape {landscape}");
let r = read_dxf(dxf.as_bytes()).unwrap();
let p = &r.layout("Layout1").unwrap().page;
assert_eq!((p.width_mm, p.height_mm, p.landscape), (297.0, 420.0, landscape), "DXF read back");
assert_eq!(p.paper, media);
let dwg = write(&d, "t.dwg").unwrap();
let back = String::from_utf8_lossy(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).into_owned();
assert_eq!(plot_paper_groups(&back, "Layout1"), (420.0, 297.0, rotation), "DWG, landscape {landscape}");
let r = read(&dwg, "t.dwg").unwrap();
let p = &r.layout("Layout1").unwrap().page;
assert_eq!((p.width_mm, p.height_mm, p.landscape), (297.0, 420.0, landscape), "DWG read back");
}
}
/// A file's own groups 44, 45 and 73 are written back unchanged, in DXF and DWG, whether they
/// follow ZWCAD's convention (the media's size, no turn) or AutoCAD's (a portrait size and a
/// quarter turn) for a media named wider than tall. Once the page setup changes, they no
/// longer describe it and the size and rotation are derived again.
#[test]
fn a_files_own_paper_groups_are_written_back() {
let set = |dxf: &str, (w, h, r): (f64, f64, i64)| -> String {
let mut lines: Vec<String> = dxf.lines().map(String::from).collect();
let name = (0..lines.len() - 3)
.find(|&i| {
lines[i].trim() == "100" && lines[i + 1].trim() == "AcDbLayout" && lines[i + 2].trim() == "1" && lines[i + 3].trim() == "Layout1"
})
.unwrap();
let start = (0..name).rev().find(|&i| lines[i].trim() == "100" && lines[i + 1].trim() == "AcDbPlotSettings").unwrap();
for i in (start..name).step_by(2) {
match lines[i].trim() {
"44" => lines[i + 1] = w.to_string(),
"45" => lines[i + 1] = h.to_string(),
"73" => lines[i + 1] = r.to_string(),
_ => {}
}
}
lines.join("\n")
};
// (media, groups as the program wrote them, portrait size and landscape CADCraft reads)
let cases = [
("ISO full bleed A3 (420.00 x 297.00 MM)", (420.0, 297.0, 0), (297.0, 420.0, true)),
("ISO A4 (297.00 x 210.00 MM)", (210.0, 297.0, 1), (210.0, 297.0, true)),
("ISO A4 (297.00 x 210.00 MM)", (210.0, 297.0, 0), (210.0, 297.0, false)),
];
for (media, groups, (pw, ph, landscape)) in cases {
let mut d = sample();
d.layouts[0].page.paper = media.into();
let file = set(&write_dxf(&d), groups);
let r = read_dxf(file.as_bytes()).unwrap();
let p = &r.layout("Layout1").unwrap().page;
assert_eq!((p.width_mm, p.height_mm, p.landscape), (pw, ph, landscape), "{media} {groups:?} read");
assert_eq!(plot_paper_groups(&write_dxf(&r), "Layout1"), groups, "{media} {groups:?} DXF");
let dwg = write(&r, "t.dwg").unwrap();
let back = String::from_utf8_lossy(&cadcraft_dwg::dwg_to_dxf(&dwg).unwrap()).into_owned();
assert_eq!(plot_paper_groups(&back, "Layout1"), groups, "{media} {groups:?} DWG");
let r = read(&dwg, "t.dwg").unwrap();
assert_eq!(plot_paper_groups(&write_dxf(&r), "Layout1"), groups, "{media} {groups:?} DWG read and saved again");
}
// Turned in CADCraft after reading: the stored groups no longer apply.
let mut d = sample();
d.layouts[0].page.paper = "ISO full bleed A3 (420.00 x 297.00 MM)".into();
let mut r = read_dxf(set(&write_dxf(&d), (420.0, 297.0, 0)).as_bytes()).unwrap();
r.layouts.iter_mut().find(|l| l.name == "Layout1").unwrap().page.landscape = false;
assert_eq!(plot_paper_groups(&write_dxf(&r), "Layout1"), (420.0, 297.0, 1), "portrait after the change");
}
#[test]
fn portrait_media_is_written_as_before() {
let mut d = sample();
{
let p = &mut d.layouts[0].page;
p.paper = "ISO A3 (297.00 x 420.00 MM)".into();
p.width_mm = 297.0;
p.height_mm = 420.0;
p.landscape = true;
}
assert_eq!(plot_paper_groups(&write_dxf(&d), "Layout1"), (297.0, 420.0, 1));
}
// ---------------------------------------------------------------------------------------------
// Round trips of styles, overrides, associativity, constraints and tables.
+3 -1
View File
@@ -256,7 +256,9 @@ pub(super) fn assert_layouts(a: &Drawing, b: &Drawing, context: &str) {
for layout in &a.layouts {
let back = b.layouts.iter().find(|l| l.name == layout.name).expect("layout must survive");
assert_eq!(back.tab_order, layout.tab_order, "{context}.{}.tab_order", layout.name);
assert_eq!(back.page, layout.page, "{context}.{}.page", layout.name);
// The groups 44, 45 and 73 the file stored are provenance, not a setting.
let setting = |p: &cadcraft_doc::PageSetup| cadcraft_doc::PageSetup { file_paper: None, ..p.clone() };
assert_eq!(setting(&back.page), setting(&layout.page), "{context}.{}.page", layout.name);
assert_eq!(back.view, layout.view, "{context}.{}.view", layout.name);
compare::entities(a, b, &layout.entities, &back.entities, &format!("{context}.layout[{}]", layout.name));
}
@@ -111,7 +111,9 @@ fn layout_page_setup_and_paper_view_round_trip() {
let r = round_trip(&d);
for (a, b) in d.layouts.iter().zip(&r.layouts) {
assert_eq!(a.name, b.name);
assert_eq!(a.page, b.page, "{}", a.name);
// The groups 44, 45 and 73 the file stored are provenance, not a setting.
let read = cadcraft_doc::PageSetup { file_paper: None, ..b.page.clone() };
assert_eq!(a.page, read, "{}", a.name);
assert_eq!(a.view, b.view, "{}", a.name);
}
assert_eq!(r.layouts.len(), d.layouts.len());