From e6e2ff50ed2a8543df85a9fad83626c7a08364eb Mon Sep 17 00:00:00 2001 From: moppeeer <339764117+moppeeer@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:04:08 +0200 Subject: [PATCH] feat(engine): Document Setup sets every page's margins; refuses impossible sizes layout.documentSetup takes `margins` (a number or {top, bottom, inside, outside}) for every page and parent page, in the same undo step as the rest of the setup; with adjustLayout objects follow the new margins too. It reports the first page's margins and whether the document has a primary text frame. Widths and heights outside 0-216 in, and margins it can't read or outside 0-216 in, are refused before anything changes. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/layout.rs | 64 +++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index bd5320799..117b49a3f 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -232,7 +232,7 @@ pub fn specs() -> Vec { "Document Setup…", ["File"], Some("Cmd+Alt+P"), - "{width?, height?, pages?: count, startPage?: n, facingPages?, binding?: leftToRight|rightToLeft, intent?: print|web|mobile, bleed?, slug?: n | [top, bottom, inside, outside], adjustLayout?: bool (objects follow the new page size)} → the document setup", + "{width?, height?, pages?: count, startPage?: n, facingPages?, binding?: leftToRight|rightToLeft, intent?: print|web|mobile, margins?: n | {top, bottom, inside, outside} (every page and parent page), bleed?, slug?: n | [top, bottom, inside, outside], adjustLayout?: bool (objects follow the new page size and margins)} → the document setup (margins: the first page's)", has_doc, |s, p| { let p = p.clone(); @@ -249,6 +249,24 @@ pub fn specs() -> Vec { } }; let (bleed, slug) = (edges("bleed"), edges("slug")); + for k in ["width", "height"] { + if let Some(v) = p.get(k) + && !v.as_f64().is_some_and(|x| x > 0.0 && x <= 15552.0) + { + return Err(bad("layout.documentSetup", format!("{k}: page size out of range (0 < size ≤ 216 in)"))); + } + } + let margins: Option = match p.get("margins") { + None => None, + Some(Value::Number(n)) => n.as_f64().map(Margins::uniform), + Some(v) => serde_json::from_value(v.clone()).ok(), + }; + if margins.is_some_and(|m| ![m.top, m.bottom, m.inside, m.outside].iter().all(|v| v.is_finite() && (0.0..=15552.0).contains(v))) { + return Err(bad("layout.documentSetup", "margins: 0 to 216 in")); + } + if p.get("margins").is_some() && margins.is_none() { + return Err(bad("layout.documentSetup", "margins: a number or {top, bottom, inside, outside}")); + } let intent: Option = p.get("intent").and_then(|v| serde_json::from_value(v.clone()).ok()); let pages = p.get("pages").and_then(Value::as_u64).map(|n| n.clamp(1, 9999) as usize); let adjust = p.get("adjustLayout").and_then(Value::as_bool).unwrap_or(false); @@ -323,6 +341,13 @@ pub fn specs() -> Vec { sp.relayout(); } } + if let Some(m) = margins { + for sp in d.spreads.iter_mut().chain(d.parents.iter_mut()) { + for pg in &mut std::sync::Arc::make_mut(sp).pages { + pg.margins = m; + } + } + } d.repaginate(); if let Some(b) = before { adjust_layout(d, &b); @@ -705,13 +730,48 @@ fn document_setup(d: &designcraft_doc::Document) -> Value { let st = &d.settings; let start = d.sections.iter().find(|x| x.start == 0).and_then(|x| x.start_number).unwrap_or(1); json!({"width": st.page_width, "height": st.page_height, "pages": d.page_count(), "startPage": start, "facingPages": st.facing_pages, - "binding": if st.right_to_left_binding { "rightToLeft" } else { "leftToRight" }, "intent": st.intent, "bleed": st.bleed, "slug": st.slug}) + "binding": if st.right_to_left_binding { "rightToLeft" } else { "leftToRight" }, "intent": st.intent, "bleed": st.bleed, "slug": st.slug, + "margins": d.page(0).map(|p| p.margins), "primaryTextFrame": st.primary_text_frame}) } #[cfg(test)] mod setup_tests { use serde_json::json; + #[test] + fn document_setup_sets_every_pages_margins_in_one_step() { + let mut s = crate::Session::new(); + s.execute("file.new", &json!({"pages": 3, "margins": 36})).unwrap(); + let f = s.execute("frame.create", &json!({"rect": [36, 36, 576, 756], "content": "text"})).unwrap(); + let r = s.execute("layout.documentSetup", &json!({})).unwrap(); + assert_eq!(r["margins"], json!({"top": 36.0, "bottom": 36.0, "inside": 36.0, "outside": 36.0})); + let m = json!({"top": 72, "bottom": 54, "inside": 72, "outside": 72}); + let r = s.execute("layout.documentSetup", &json!({"margins": m, "bleed": 9, "adjustLayout": true})).unwrap(); + assert_eq!(r["margins"]["bottom"], 54.0); + let d = &s.doc().unwrap().doc; + assert!( + d.spreads.iter().chain(&d.parents).flat_map(|sp| &sp.pages).all(|pg| pg.margins.top == 72.0 && pg.margins.bottom == 54.0), + "every page and parent page" + ); + let b = d.item(designcraft_doc::ItemId(f["id"].as_u64().unwrap())).unwrap().bounds(); + assert!((b.y0 - 72.0).abs() < 1e-6, "Adjust Layout: the frame follows the margins ({b:?})"); + // One step: undo brings back the margins and the bleed. + s.execute("edit.undo", &json!({})).unwrap(); + let r = s.execute("layout.documentSetup", &json!({})).unwrap(); + assert_eq!((r["margins"]["top"].clone(), r["bleed"][0].clone()), (json!(36.0), json!(0.0))); + for bad in [ + json!({"width": 0}), + json!({"height": -5}), + json!({"width": 1e9}), + json!({"width": "wide"}), + json!({"margins": -1}), + json!({"margins": {"top": 1}}), + ] { + assert!(s.execute("layout.documentSetup", &bad).is_err(), "{bad}"); + } + assert_eq!(s.execute("layout.documentSetup", &json!({})).unwrap()["width"], 612.0, "refused values change nothing"); + } + #[test] fn document_setup_pages_start_bleed_slug() { let mut s = crate::Session::new();