From cdd32a5066a82156a248ee4712b0e7059fce9b1e Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 10 Oct 2026 21:24:02 +0000 Subject: [PATCH] Layout: right-to-left binding puts parent page items on the right side A right-to-left bound spread lists its right page first, parent spreads included. The parent page shown behind a document page was picked by list index (first = left), so every page of an imported right-to-left document showed the other side's parent items. Pick it by position instead: the leftmost parent page for a left page, the rightmost for a right page. New parent spreads in a right-to-left document list their pages right first, and changing the binding reorders existing parent spreads, so IDML export writes them in binding order. Spread bleed on the canvas and in spread PDF export takes the outer edges by position as well. --- crates/doc/src/build.rs | 18 +++++- crates/doc/src/page.rs | 23 ++++++- crates/engine/src/cmd/layout.rs | 33 ++++++++++ crates/idml/src/tests.rs | 1 + crates/idml/src/tests/rtl_binding.rs | 91 ++++++++++++++++++++++++++++ crates/pdf/src/export.rs | 6 +- crates/tools/src/snap.rs | 11 +--- crates/ui-egui/src/canvas.rs | 7 ++- 8 files changed, 172 insertions(+), 18 deletions(-) create mode 100644 crates/idml/src/tests/rtl_binding.rs diff --git a/crates/doc/src/build.rs b/crates/doc/src/build.rs index c274f2130..25bcb0bcd 100644 --- a/crates/doc/src/build.rs +++ b/crates/doc/src/build.rs @@ -236,6 +236,9 @@ impl Document { if n == 2 { pages[0].side = PageSide::Left; pages[1].side = PageSide::Right; + if self.settings.right_to_left_binding { + pages.reverse(); + } } let id = SpreadId(self.alloc()); let mut sp = Spread { @@ -250,6 +253,18 @@ impl Document { id } + /// List the pages of each facing parent spread in binding order: left then right, or right + /// then left when bound right to left. Positions stay, so the parent items keep their pages. + pub fn order_parent_pages(&mut self) { + let first = if self.settings.right_to_left_binding { PageSide::Right } else { PageSide::Left }; + for sp in &mut self.parents { + let pair = matches!(sp.pages.as_slice(), [a, b] if a.side != b.side && a.side != PageSide::Single && b.side != PageSide::Single); + if pair && sp.pages.first().is_some_and(|p| p.side != first) { + Arc::make_mut(sp).pages.reverse(); + } + } + } + pub fn parent_index(&self, id: SpreadId) -> Option { self.parents.iter().position(|p| p.id == id) } @@ -549,8 +564,7 @@ impl Document { let pi = self.parent_index(page.parent?)?; let ps = &self.parents[pi]; // A parent spread can come without pages from a file. - let idx = if ps.pages.len() >= 2 && page.side == PageSide::Left { 0 } else { ps.pages.len().checked_sub(1)? }; - Some((pi, idx)) + Some((pi, ps.parent_page_for_side(page.side)?)) } } diff --git a/crates/doc/src/page.rs b/crates/doc/src/page.rs index f1eaa48ad..fcc788fa2 100644 --- a/crates/doc/src/page.rs +++ b/crates/doc/src/page.rs @@ -296,6 +296,25 @@ impl Spread { None => self.bounds().center().x, } } + /// Indices of the leftmost and rightmost pages. A right-to-left bound spread lists its right + /// page first, so list order says nothing about position. + pub fn outer_page_indices(&self) -> Option<(usize, usize)> { + let mut it = self.pages.iter().enumerate(); + let (first, _) = it.next()?; + Some(it.fold((first, first), |(l, r), (i, p)| (if p.x < self.pages[l].x { i } else { l }, if p.x > self.pages[r].x { i } else { r }))) + } + /// The parent page (index into this parent spread) shown behind a document page on `side`: + /// the leftmost page for a left page and the rightmost for a right page, whichever binding + /// ordered the list; the last page for a single-sided page or a one-page parent. + pub fn parent_page_for_side(&self, side: PageSide) -> Option { + let (l, r) = self.outer_page_indices()?; + match side { + _ if self.pages.len() < 2 => Some(l), + PageSide::Left => Some(l), + PageSide::Right => Some(r), + PageSide::Single => self.pages.len().checked_sub(1), + } + } /// Index of the page under spread x (nearest page for the pasteboard). pub fn page_at_x(&self, x: f64) -> Option { if self.pages.is_empty() { @@ -303,9 +322,7 @@ impl Spread { } self.pages.iter().position(|p| x >= p.x && x < p.x + p.width).or_else(|| { // The pasteboard: the page nearest that side. - let (l, r) = (0..self.pages.len()).fold((0, 0), |(l, r), i| { - (if self.pages[i].x < self.pages[l].x { i } else { l }, if self.pages[i].x > self.pages[r].x { i } else { r }) - }); + let (l, r) = self.outer_page_indices()?; Some(if x < self.pages[l].x { l } else { r }) }) } diff --git a/crates/engine/src/cmd/layout.rs b/crates/engine/src/cmd/layout.rs index bd5320799..01ab906bd 100644 --- a/crates/engine/src/cmd/layout.rs +++ b/crates/engine/src/cmd/layout.rs @@ -259,6 +259,7 @@ pub fn specs() -> Vec { } if let Some(b) = p.get("binding").and_then(Value::as_str) { d.settings.right_to_left_binding = b == "rightToLeft"; + d.order_parent_pages(); } if let Some(b) = bleed { d.settings.bleed = b; @@ -760,6 +761,38 @@ mod setup_tests { assert_eq!(d.spreads[1].pages.iter().map(|p| (p.side, p.x)).collect::>(), vec![(Left, 0.0), (Right, w)]); } + /// Bound right to left, a parent spread lists its right page first; each document page still + /// shows the parent page on its own side, at its own position, also after an IDML round trip. + #[test] + fn right_to_left_binding_shows_the_parent_page_of_the_same_side() { + use designcraft_doc::PageSide::{Left, Right}; + let mut s = crate::Session::new(); + s.execute("file.new", &json!({"pages": 4})).unwrap(); + s.execute("layout.documentSetup", &json!({"binding": "rightToLeft"})).unwrap(); + let w = s.doc().unwrap().doc.settings.page_width; + let parent = json!({"kind": "parent", "index": 0}); + for (rect, text) in [(json!([72, 36, 200, 60]), "left head"), (json!([w + 72.0, 36, w + 200.0, 60]), "right head")] { + s.execute("frame.create", &json!({"spread": parent, "rect": rect, "content": "text", "text": text})).unwrap(); + } + let d = s.doc().unwrap().doc.clone(); + assert_eq!(d.parents[0].pages.iter().map(|p| (p.side, p.x)).collect::>(), vec![(Right, w), (Left, 0.0)]); + let docs = [(*d).clone(), designcraft_idml::import_idml(&designcraft_idml::export_idml(&d)).unwrap()]; + for d in docs { + let mut s = crate::Session::new(); + s.add_document(crate::DocState::new(d, None)); + for (page, side, text) in [(0, Left, "left head"), (1, Right, "right head"), (2, Left, "left head"), (3, Right, "right head")] { + let r = s.execute("layout.overrideParentItems", &json!({"page": page})).unwrap(); + let d = &s.doc().unwrap().doc; + let ids = r["ids"].as_array().unwrap(); + assert_eq!(ids.len(), 1, "page {}", page + 1); + let it = d.item(designcraft_doc::ItemId(ids[0].as_u64().unwrap())).unwrap(); + let story = d.story(it.text_frame().unwrap().story).unwrap(); + assert_eq!((d.page(page).unwrap().side, story.text.trim_end()), (side, text), "page {}", page + 1); + assert_eq!(d.page_of_item(it.id), Some(page), "page {}", page + 1); + } + } + } + /// Multi-column frames filled the left column first in a right-to-left document (#100): /// stories made in a right-to-left-bound document start their columns on the right. #[test] diff --git a/crates/idml/src/tests.rs b/crates/idml/src/tests.rs index 7b747587a..a13858e8e 100644 --- a/crates/idml/src/tests.rs +++ b/crates/idml/src/tests.rs @@ -8,6 +8,7 @@ use designcraft_geom::{Rect, shapes}; use super::*; mod decorations; +mod rtl_binding; fn zip_files(files: &[(&str, &str)]) -> Vec { use zip::write::SimpleFileOptions; diff --git a/crates/idml/src/tests/rtl_binding.rs b/crates/idml/src/tests/rtl_binding.rs new file mode 100644 index 000000000..3451aa2d4 --- /dev/null +++ b/crates/idml/src/tests/rtl_binding.rs @@ -0,0 +1,91 @@ +use designcraft_doc::PageSide::{self, Left, Right}; + +use super::*; + +const NS: &str = r#"xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" DOMVersion="16.0""#; + +fn rect_item(id: &str, x0: f64, x1: f64) -> String { + let pts: String = [(x0, -280.0), (x0, -260.0), (x1, -260.0), (x1, -280.0)] + .iter() + .map(|(x, y)| format!(r#""#)) + .collect(); + format!( + r#"{pts}"# + ) +} + +/// Bound right to left, the first page of every spread (parent spreads too) lies right of the +/// spine and the second left of it. The parent's right page holds a 100 pt wide item, its left +/// page a 50 pt one. Five pages: 1 alone, then 2–3 and 4–5. +fn right_to_left_fixture() -> Vec { + let page = |id: &str, x: f64, master: bool| { + let applied = if master { String::new() } else { r#" AppliedMaster="m1""#.to_string() }; + format!(r#""#) + }; + let master = format!( + r#"{}{}{}{}"#, + page("mp1", 0.0, true), + page("mp2", -400.0, true), + rect_item("right", 50.0, 150.0), + rect_item("left", -350.0, -300.0), + ); + let spread = |id: &str, pages: &[(&str, f64)]| { + let ps: String = pages.iter().map(|(p, x)| page(p, *x, false)).collect(); + format!(r#"{ps}"#, pages.len()) + }; + let map = format!( + r#""# + ); + let prefs = format!( + r#""# + ); + zip_files(&[ + ("designmap.xml", &map), + ("Resources/Preferences.xml", &prefs), + ("MasterSpreads/m1.xml", &master), + ("Spreads/sp0.xml", &spread("sp0", &[("p1", 0.0)])), + ("Spreads/sp1.xml", &spread("sp1", &[("p2", 0.0), ("p3", -400.0)])), + ("Spreads/sp2.xml", &spread("sp2", &[("p4", 0.0), ("p5", -400.0)])), + ]) +} + +/// The parent items a document page shows, as (width, bounds on the document spread). +fn shown_parent_items(d: &Document, abs: usize) -> Vec<(f64, Rect)> { + let (si, pi) = d.page_loc(abs).unwrap(); + let page = &d.spreads[si].pages[pi]; + let (ppi, ppage) = d.parent_page_for(abs).unwrap(); + let parent = &d.parents[ppi]; + let dx = page.x - parent.pages[ppage].x; + parent + .items + .iter() + .filter(|it| parent.page_at_x(it.bounds().center().x) == Some(ppage)) + .map(|it| { + let b = it.bounds(); + (b.width(), Rect::new(b.x0 + dx, b.y0, b.x1 + dx, b.y1)) + }) + .collect() +} + +fn assert_parent_items_follow_page_sides(d: &Document) { + assert!(d.settings.right_to_left_binding); + let sides: Vec = d.parents[0].pages.iter().map(|p| p.side).collect(); + assert_eq!(sides, vec![Right, Left], "the parent spread lists its right page first"); + for (abs, side, width) in [(0, Right, 100.0), (1, Right, 100.0), (2, Left, 50.0), (3, Right, 100.0), (4, Left, 50.0)] { + let page = d.page(abs).unwrap(); + assert_eq!(page.side, side, "page {}", abs + 1); + let shown = shown_parent_items(d, abs); + assert_eq!(shown.len(), 1, "page {}", abs + 1); + let (w, b) = shown[0]; + assert!((w - width).abs() < 1e-6, "page {} shows the {width} pt item, not {w}", abs + 1); + assert!(b.x0 >= page.x - 1e-6 && b.x1 <= page.x + page.width + 1e-6, "page {}: {b:?} lies on the page", abs + 1); + } +} + +#[test] +fn right_to_left_parent_pages_follow_the_document_page_side() { + let d = import_idml(&right_to_left_fixture()).unwrap(); + assert_parent_items_follow_page_sides(&d); + let again = import_idml(&export_idml(&d)).unwrap(); + assert_parent_items_follow_page_sides(&again); +} diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index d92fc242b..246c35956 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -73,8 +73,10 @@ fn sheets(doc: &Document, opts: &PdfOptions) -> Result> { } seen_spreads.push(si); let first = doc.first_page_of_spread(si); - let (l, _) = sp.pages.first().map(|p| bleed_lr(p.side, b)).unwrap_or((b[2], b[3])); - let (_, r) = sp.pages.last().map(|p| bleed_lr(p.side, b)).unwrap_or((b[2], b[3])); + // The outer edges by position: a right-to-left spread lists its right page first. + let outer = sp.outer_page_indices(); + let (l, _) = outer.and_then(|(l, _)| sp.pages.get(l)).map(|p| bleed_lr(p.side, b)).unwrap_or((b[2], b[3])); + let (_, r) = outer.and_then(|(_, r)| sp.pages.get(r)).map(|p| bleed_lr(p.side, b)).unwrap_or((b[2], b[3])); let names: Vec = (0..sp.pages.len()).map(|i| doc.page_name(first + i)).collect(); (sp.bounds(), l, r, names.join("–")) } else { diff --git a/crates/tools/src/snap.rs b/crates/tools/src/snap.rs index 6d5c8f6a5..fe8c6b6c9 100644 --- a/crates/tools/src/snap.rs +++ b/crates/tools/src/snap.rs @@ -255,7 +255,7 @@ fn push_parent_rotation_chain( return; } let next = parent.parent.as_ref().and_then(|info| info.based_on); - if let Some(page_idx) = shown_parent_page_index(parent.pages.len(), doc_side) { + if let Some(page_idx) = parent.parent_page_for_side(doc_side) { for item in &parent.items { if overridden.contains(&item.id) { continue; @@ -1053,7 +1053,7 @@ fn push_parent_length_chain( return; } let next = parent.parent.as_ref().and_then(|info| info.based_on); - if let Some(page_idx) = shown_parent_page_index(parent.pages.len(), doc_side) { + if let Some(page_idx) = parent.parent_page_for_side(doc_side) { for item in &parent.items { if overridden.contains(&item.id) { continue; @@ -1267,7 +1267,7 @@ fn offer_parent_chain( return; } let next = parent.parent.as_ref().and_then(|info| info.based_on); - if let Some(page_idx) = shown_parent_page_index(parent.pages.len(), doc_side) + if let Some(page_idx) = parent.parent_page_for_side(doc_side) && let Some(parent_page) = parent.pages.get(page_idx) { let dx = doc_x - parent_page.x; @@ -1291,11 +1291,6 @@ fn offer_parent_chain( offer_parent_chain(best, cx, req, flags, axis, tol, doc_x, doc_side, overridden, next_index, seen); } -/// Left page of a facing parent, otherwise the last page. Same choice as `Document::parent_page_for`. -fn shown_parent_page_index(page_count: usize, side: PageSide) -> Option { - if page_count >= 2 && side == PageSide::Left { Some(0) } else { page_count.checked_sub(1) } -} - fn shift_x(r: Rect, dx: f64) -> Rect { Rect::new(r.x0 + dx, r.y0, r.x1 + dx, r.y1) } diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 34dd61442..39f2b605e 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -593,9 +593,10 @@ fn draw_guides(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Document let rect = |r: DRect| xf.rect(m.transform_rect_bbox(r)); // Bleed: one rectangle around the spread (inside bleed only applies at the spread's outer edges). let b = s.bleed; - if app.ui.guides && b.iter().any(|v| *v > 0.0) && !sp.pages.is_empty() { - let first = &sp.pages[0]; - let last = &sp.pages[sp.pages.len() - 1]; + if app.ui.guides + && b.iter().any(|v| *v > 0.0) + && let Some((first, last)) = sp.outer_page_indices().and_then(|(l, r)| Some((sp.pages.get(l)?, sp.pages.get(r)?))) + { let l = if first.side == PageSide::Left { b[3] } else { b[2] }; let r = if last.side == PageSide::Right || last.side == PageSide::Single { b[3] } else { b[2] }; let sb = sp.bounds();