mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 13:46:52 -06:00
Facing pages take their side from their section's page numbers
Chris Nielsen's "Numbering Page Sections" (7:03): starting a section at page 1 on a left page (the spread 8–9 selected) turns it into a lone right-hand page 1 in InDesign, and the pages after it shuffle. DesignCraft kept the page on the left, so odd page numbers sat on left pages. - Document::repaginate gives each facing page the side of its number in its section (odd: right, or left bound right to left). Document Setup's Start Page # is one case of it. - layout.section repaginates when a section starts, changes or is removed; objects go with their pages, in one undo step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
43a46c04bf
commit
188c4f7007
@@ -128,6 +128,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-14** · In a facing document, pages take their side from their section's page numbers, as InDesign shuffles them: a section that starts at 1 on a left page moves it to the right and the pages after it follow, with their objects. No readiness percentage change.
|
||||
- **2026-10-12** · Text threads by hand: click a selected text frame's in or out port to load
|
||||
the text cursor, then click a frame to thread it, click empty space for a column-wide frame or
|
||||
drag one (it snaps like frame drawing); Shift-click autoflows, Alt-click stays loaded on the
|
||||
|
||||
+15
-5
@@ -267,15 +267,26 @@ impl Document {
|
||||
|
||||
/// Redistribute document pages into spreads (facing: page 1 alone on the right, then pairs).
|
||||
/// Items move with the page their centre is on. Spreads with `allow_shuffle == false` are kept.
|
||||
/// Facing pages take their side from their page number, as InDesign shuffles them: an odd
|
||||
/// number is a right page (a left page bound right to left), so a section that starts at 1 on
|
||||
/// a left page moves it to the right and the pages after it follow.
|
||||
pub fn repaginate(&mut self) {
|
||||
// Collect pages with their items (items in page-relative coordinates).
|
||||
let odd: Vec<bool> = (0..self.page_count()).map(|i| self.page_number(i) % 2 == 1).collect();
|
||||
// Collect pages with their items (items in page-relative coordinates) and their numbers' parity.
|
||||
let mut pages: Vec<(Page, Vec<Arc<Item>>)> = Vec::new();
|
||||
let mut parity: Vec<bool> = Vec::new();
|
||||
let mut kept: Vec<(usize, Arc<Spread>)> = Vec::new();
|
||||
let mut abs = 0;
|
||||
for sp in self.spreads.drain(..) {
|
||||
if !sp.allow_shuffle && sp.pages.len() > 1 {
|
||||
abs += sp.pages.len();
|
||||
kept.push((pages.len(), sp));
|
||||
continue;
|
||||
}
|
||||
for _ in &sp.pages {
|
||||
parity.push(odd.get(abs).copied().unwrap_or(abs % 2 == 0));
|
||||
abs += 1;
|
||||
}
|
||||
let mut sp = Arc::unwrap_or_clone(sp);
|
||||
let mut per: Vec<Vec<Arc<Item>>> = vec![Vec::new(); sp.pages.len()];
|
||||
for it in std::mem::take(&mut sp.items) {
|
||||
@@ -326,13 +337,12 @@ impl Document {
|
||||
}
|
||||
spreads.push(sp);
|
||||
};
|
||||
// An even start page number (Document Setup › Start Page #) begins with a left page.
|
||||
let shift = usize::from(self.sections.iter().find(|x| x.start == 0).and_then(|x| x.start_number).is_some_and(|n| n % 2 == 0));
|
||||
for (i, (mut p, items)) in pages.into_iter().enumerate() {
|
||||
if facing {
|
||||
// Page i (0-based) is a right page when its number is odd (1, 3, 5 …).
|
||||
// A page is a right page when its number is odd (1, 3, 5 …), counting each
|
||||
// section from its start number (Document Setup › Start Page # included).
|
||||
// Bound right to left, odd pages are left pages and spreads start on the right.
|
||||
let odd = (i + shift) % 2 == 0;
|
||||
let odd = parity.get(i).copied().unwrap_or(i % 2 == 0);
|
||||
p.side = if odd != rtl { PageSide::Right } else { PageSide::Left };
|
||||
let starts = if rtl { PageSide::Right } else { PageSide::Left };
|
||||
if p.side == starts && !cur.is_empty() {
|
||||
|
||||
@@ -398,6 +398,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
s.edit(|d, _| {
|
||||
d.sections.retain(|x| x.start != start || start == 0);
|
||||
if p.get("remove").and_then(Value::as_bool).unwrap_or(false) && start != 0 {
|
||||
// The pages after it follow the previous section's numbers again.
|
||||
d.repaginate();
|
||||
return ok();
|
||||
}
|
||||
let mut sec = d.sections.iter().find(|x| x.start == start).cloned().unwrap_or(designcraft_doc::Section {
|
||||
@@ -428,6 +430,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
d.sections.retain(|x| x.start != start);
|
||||
d.sections.push(sec);
|
||||
d.sections.sort_by_key(|x| x.start);
|
||||
// Facing pages take their sides from their numbers: odd on the right.
|
||||
d.repaginate();
|
||||
Ok(json!({"names": (0..d.page_count()).map(|i| d.page_name(i)).collect::<Vec<_>>()}))
|
||||
})
|
||||
}
|
||||
@@ -921,3 +925,89 @@ mod adjust_tests {
|
||||
assert_eq!(b, b2);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod section_side_tests {
|
||||
use serde_json::json;
|
||||
|
||||
use crate::Session;
|
||||
|
||||
/// Each spread's pages as (name, side), side `L`, `R` or `S`.
|
||||
fn spreads(s: &Session) -> Vec<Vec<(String, char)>> {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let mut abs = 0;
|
||||
d.spreads
|
||||
.iter()
|
||||
.map(|sp| {
|
||||
sp.pages
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let side = match p.side {
|
||||
designcraft_doc::PageSide::Left => 'L',
|
||||
designcraft_doc::PageSide::Right => 'R',
|
||||
designcraft_doc::PageSide::Single => 'S',
|
||||
};
|
||||
abs += 1;
|
||||
(d.page_name(abs - 1), side)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn pages(v: &[&[(&str, char)]]) -> Vec<Vec<(String, char)>> {
|
||||
v.iter().map(|sp| sp.iter().map(|(n, c)| (n.to_string(), *c)).collect()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_section_starting_at_one_on_a_left_page_moves_it_to_the_right() {
|
||||
// Chris Nielsen's "Numbering Page Sections" (7:03): with the spread 8–9 selected, the
|
||||
// section starts on page 8, which becomes a lone right-hand page 1; the pages after it
|
||||
// shuffle and their objects go with them. Undo puts them back.
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 612, "height": 792, "facingPages": true, "pages": 10})).unwrap();
|
||||
let id = s.execute("frame.create", &json!({"spread": 4, "rect": [700, 100, 800, 200], "content": "text", "text": "TOPIC"})).unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let page_of = |s: &Session| {
|
||||
let d = &s.doc().unwrap().doc;
|
||||
d.page_of_item(designcraft_doc::ItemId(id)).unwrap()
|
||||
};
|
||||
assert_eq!(page_of(&s), 8, "page 9, the right page of the spread 8–9");
|
||||
let before = spreads(&s);
|
||||
s.execute("layout.section", &json!({"page": 8, "startNumber": 1})).unwrap();
|
||||
assert_eq!(
|
||||
spreads(&s),
|
||||
pages(&[
|
||||
&[("1", 'R')],
|
||||
&[("2", 'L'), ("3", 'R')],
|
||||
&[("4", 'L'), ("5", 'R')],
|
||||
&[("6", 'L'), ("7", 'R')],
|
||||
&[("1", 'R')],
|
||||
&[("2", 'L'), ("3", 'R')],
|
||||
])
|
||||
);
|
||||
assert_eq!(page_of(&s), 8, "the object stays on its page, now the left page of a spread");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(spreads(&s), before);
|
||||
// On a right page (the video's page 9) nothing moves; an even start on a right page moves
|
||||
// it to the left.
|
||||
s.execute("layout.section", &json!({"page": 9, "startNumber": 1})).unwrap();
|
||||
assert_eq!(spreads(&s)[4], pages(&[&[("8", 'L'), ("1", 'R')]])[0]);
|
||||
s.execute("layout.section", &json!({"page": 9, "startNumber": 2})).unwrap();
|
||||
assert_eq!(spreads(&s)[4..], pages(&[&[("8", 'L')], &[("2", 'L'), ("3", 'R')]]));
|
||||
// Removing the section numbers the pages on from 9 again.
|
||||
s.execute("layout.section", &json!({"page": 9, "remove": true})).unwrap();
|
||||
assert_eq!(spreads(&s), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_sided_documents_keep_their_pages() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"facingPages": false, "pages": 4})).unwrap();
|
||||
s.execute("layout.section", &json!({"page": 2, "startNumber": 1})).unwrap();
|
||||
let sp = spreads(&s);
|
||||
assert_eq!(sp.len(), 4);
|
||||
assert!(sp.iter().all(|p| p.len() == 1 && p[0].1 == 'S'), "{sp:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user