mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-25 18:56:50 -06:00
682 lines
28 KiB
Rust
682 lines
28 KiB
Rust
//! Creating documents (File → New → Document), presets, and page/spread structure edits
|
||
//! (insert/delete/move pages, repagination into spreads, parents).
|
||
|
||
use std::collections::BTreeMap;
|
||
use std::sync::Arc;
|
||
|
||
use designcraft_geom::{Affine, Unit, units};
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
use crate::ids::{ItemId, LayerId, PageId, SpreadId};
|
||
use crate::item::Item;
|
||
use crate::page::{Columns, Guide, Margins, Orientation, Page, PageSide, ParentInfo, Section, Spread};
|
||
use crate::styles::Styles;
|
||
use crate::{DataMerge, DocError, DocSettings, Document, Intent, LAYER_COLORS, Layer, ParaFormat, Result, SpreadRef};
|
||
|
||
/// A New Document preset.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct Preset {
|
||
pub name: &'static str,
|
||
pub intent: Intent,
|
||
pub width: f64,
|
||
pub height: f64,
|
||
pub units: Unit,
|
||
}
|
||
|
||
const MM: f64 = units::PT_PER_MM;
|
||
|
||
pub const PRESETS: &[Preset] = &[
|
||
Preset { name: "Letter", intent: Intent::Print, width: 612.0, height: 792.0, units: Unit::Picas },
|
||
Preset { name: "Legal", intent: Intent::Print, width: 612.0, height: 1008.0, units: Unit::Picas },
|
||
Preset { name: "Tabloid", intent: Intent::Print, width: 792.0, height: 1224.0, units: Unit::Picas },
|
||
Preset { name: "Letter - Half", intent: Intent::Print, width: 396.0, height: 612.0, units: Unit::Picas },
|
||
Preset { name: "A3", intent: Intent::Print, width: 297.0 * MM, height: 420.0 * MM, units: Unit::Millimeters },
|
||
Preset { name: "A4", intent: Intent::Print, width: 210.0 * MM, height: 297.0 * MM, units: Unit::Millimeters },
|
||
Preset { name: "A5", intent: Intent::Print, width: 148.0 * MM, height: 210.0 * MM, units: Unit::Millimeters },
|
||
Preset { name: "A6", intent: Intent::Print, width: 105.0 * MM, height: 148.0 * MM, units: Unit::Millimeters },
|
||
Preset { name: "B5", intent: Intent::Print, width: 176.0 * MM, height: 250.0 * MM, units: Unit::Millimeters },
|
||
Preset { name: "Business Card", intent: Intent::Print, width: 252.0, height: 144.0, units: Unit::Picas },
|
||
Preset { name: "Postcard", intent: Intent::Print, width: 432.0, height: 288.0, units: Unit::Picas },
|
||
Preset { name: "Web 1920 × 1080", intent: Intent::Web, width: 1920.0, height: 1080.0, units: Unit::Pixels },
|
||
Preset { name: "Web 1366 × 768", intent: Intent::Web, width: 1366.0, height: 768.0, units: Unit::Pixels },
|
||
Preset { name: "Web 1024 × 768", intent: Intent::Web, width: 1024.0, height: 768.0, units: Unit::Pixels },
|
||
Preset { name: "Mobile 1080 × 1920", intent: Intent::Mobile, width: 1080.0, height: 1920.0, units: Unit::Pixels },
|
||
Preset { name: "Mobile 390 × 844", intent: Intent::Mobile, width: 390.0, height: 844.0, units: Unit::Pixels },
|
||
Preset { name: "Tablet 1024 × 1366", intent: Intent::Mobile, width: 1024.0, height: 1366.0, units: Unit::Pixels },
|
||
];
|
||
|
||
/// File → New → Document parameters.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
#[serde(rename_all = "camelCase", default)]
|
||
pub struct NewDocument {
|
||
pub title: String,
|
||
pub intent: Intent,
|
||
pub width: f64,
|
||
pub height: f64,
|
||
pub pages: usize,
|
||
pub start_page: u32,
|
||
pub facing_pages: bool,
|
||
pub primary_text_frame: bool,
|
||
pub columns: u32,
|
||
pub gutter: f64,
|
||
pub margins: Margins,
|
||
pub bleed: [f64; 4],
|
||
pub slug: [f64; 4],
|
||
pub units: Unit,
|
||
}
|
||
|
||
impl Default for NewDocument {
|
||
fn default() -> Self {
|
||
NewDocument {
|
||
title: "Untitled-1".into(),
|
||
intent: Intent::Print,
|
||
width: 612.0,
|
||
height: 792.0,
|
||
pages: 1,
|
||
start_page: 1,
|
||
facing_pages: true,
|
||
primary_text_frame: false,
|
||
columns: 1,
|
||
gutter: 12.0,
|
||
margins: Margins::uniform(36.0),
|
||
bleed: [0.0; 4],
|
||
slug: [0.0; 4],
|
||
units: Unit::Picas,
|
||
}
|
||
}
|
||
}
|
||
|
||
impl NewDocument {
|
||
pub fn from_preset(name: &str) -> Option<Self> {
|
||
let p = PRESETS.iter().find(|p| p.name.eq_ignore_ascii_case(name))?;
|
||
let web = p.intent != Intent::Print;
|
||
Some(NewDocument {
|
||
intent: p.intent,
|
||
width: p.width,
|
||
height: p.height,
|
||
units: p.units,
|
||
facing_pages: !web,
|
||
margins: if p.units == Unit::Millimeters { Margins::uniform(12.7 * MM) } else { Margins::uniform(36.0) },
|
||
..Default::default()
|
||
})
|
||
}
|
||
}
|
||
|
||
impl Document {
|
||
/// Build a new document.
|
||
pub fn new(nd: &NewDocument) -> Document {
|
||
let settings = DocSettings {
|
||
intent: nd.intent,
|
||
page_width: nd.width,
|
||
page_height: nd.height,
|
||
facing_pages: nd.facing_pages,
|
||
primary_text_frame: nd.primary_text_frame,
|
||
bleed: nd.bleed,
|
||
slug: nd.slug,
|
||
horizontal_units: nd.units,
|
||
vertical_units: nd.units,
|
||
blend_space: if nd.intent == Intent::Print { crate::BlendSpace::Cmyk } else { crate::BlendSpace::Rgb },
|
||
glyph_fallback: false,
|
||
..Default::default()
|
||
};
|
||
let mut d = Document {
|
||
title: nd.title.clone(),
|
||
settings,
|
||
spreads: vec![],
|
||
parents: vec![],
|
||
layers: vec![],
|
||
stories: BTreeMap::new(),
|
||
styles: Arc::new(Styles::default()),
|
||
swatches: designcraft_color::default_swatches(),
|
||
color_groups: vec![],
|
||
conditions: vec![],
|
||
inks: Default::default(),
|
||
stroke_styles: vec![],
|
||
articles: vec![],
|
||
xml: Default::default(),
|
||
endnote_options: Default::default(),
|
||
endnote_story: None,
|
||
sections: vec![Section {
|
||
start: 0,
|
||
start_number: Some(nd.start_page.max(1)),
|
||
style: Default::default(),
|
||
prefix: String::new(),
|
||
marker: String::new(),
|
||
include_prefix: false,
|
||
}],
|
||
assets: BTreeMap::new(),
|
||
hyperlinks: vec![],
|
||
data_merge: DataMerge::default(),
|
||
bookmarks: vec![],
|
||
user_words: vec![],
|
||
hyphenation_exceptions: vec![],
|
||
toc: None,
|
||
text_variables: crate::vars::defaults(),
|
||
footnote_options: Default::default(),
|
||
xref_formats: crate::xref::default_formats(),
|
||
index: None,
|
||
created: crate::vars::now(),
|
||
modified: 0,
|
||
next_id: 0,
|
||
font_scope: 0,
|
||
};
|
||
let lid = LayerId(d.alloc());
|
||
d.layers.push(Layer {
|
||
id: lid,
|
||
name: "Layer 1".into(),
|
||
color: LAYER_COLORS[0].1,
|
||
visible: true,
|
||
locked: false,
|
||
printable: true,
|
||
show_guides: true,
|
||
suppress_wrap_when_hidden: false,
|
||
});
|
||
// A-Parent.
|
||
let parent_id = d.add_parent("A", "Parent", nd.columns, nd.gutter, nd.margins);
|
||
// Document pages.
|
||
let pages: Vec<Page> = (0..nd.pages.max(1)).map(|_| d.make_page(Some(parent_id), nd.columns, nd.gutter, nd.margins)).collect();
|
||
d.spreads = vec![Arc::new(Spread { id: SpreadId(d.alloc()), pages, items: vec![], parent: None, allow_shuffle: true })];
|
||
d.repaginate();
|
||
if nd.primary_text_frame {
|
||
d.add_primary_frames(nd.columns, nd.gutter);
|
||
}
|
||
d
|
||
}
|
||
|
||
/// Primary Text Frame: a margin-sized frame on every page, threaded into one story.
|
||
fn add_primary_frames(&mut self, columns: u32, gutter: f64) {
|
||
let layer = self.default_layer();
|
||
let mut prev: Option<ItemId> = None;
|
||
let mut story = None;
|
||
for page in 0..self.page_count() {
|
||
let Some((si, pi)) = self.page_loc(page) else { continue };
|
||
let r = self.spreads[si].pages[pi].margin_rect();
|
||
let Ok((fid, sid)) = self.add_text_frame(SpreadRef::Doc(si), r, layer, "", ParaFormat::default()) else { continue };
|
||
if let Some(it) = self.item_mut(fid)
|
||
&& let Some(tf) = it.text_frame_mut()
|
||
{
|
||
tf.options.columns = columns.max(1);
|
||
tf.options.gutter = gutter;
|
||
}
|
||
match prev {
|
||
Some(p) => {
|
||
let _ = self.thread(p, fid);
|
||
}
|
||
None => story = Some(sid),
|
||
}
|
||
prev = Some(fid);
|
||
}
|
||
self.settings.primary_story = story;
|
||
}
|
||
|
||
fn make_page(&mut self, parent: Option<SpreadId>, columns: u32, gutter: f64, margins: Margins) -> Page {
|
||
Page {
|
||
id: PageId(self.alloc()),
|
||
width: self.settings.page_width,
|
||
height: self.settings.page_height,
|
||
x: 0.0,
|
||
margins,
|
||
columns: Columns { count: columns.max(1), gutter, positions: None },
|
||
parent,
|
||
side: PageSide::Single,
|
||
overridden: vec![],
|
||
guides: vec![],
|
||
show_parent_items: true,
|
||
liquid: Default::default(),
|
||
transition: None,
|
||
view_rotation: 0,
|
||
show_parent_overlay: false,
|
||
}
|
||
}
|
||
|
||
/// Add a parent spread (`prefix`-`name`) and return its id.
|
||
pub fn add_parent(&mut self, prefix: &str, name: &str, columns: u32, gutter: f64, margins: Margins) -> SpreadId {
|
||
let n = if self.settings.facing_pages { 2 } else { 1 };
|
||
let mut pages: Vec<Page> = (0..n).map(|_| self.make_page(None, columns, gutter, margins)).collect();
|
||
if n == 2 {
|
||
pages[0].side = PageSide::Left;
|
||
pages[1].side = PageSide::Right;
|
||
}
|
||
let id = SpreadId(self.alloc());
|
||
let mut sp = Spread {
|
||
id,
|
||
pages,
|
||
items: vec![],
|
||
parent: Some(ParentInfo { prefix: prefix.into(), name: name.into(), based_on: None }),
|
||
allow_shuffle: true,
|
||
};
|
||
sp.relayout();
|
||
self.parents.push(Arc::new(sp));
|
||
id
|
||
}
|
||
|
||
pub fn parent_index(&self, id: SpreadId) -> Option<usize> {
|
||
self.parents.iter().position(|p| p.id == id)
|
||
}
|
||
|
||
/// The next free parent prefix (A, B, C…).
|
||
pub fn next_parent_prefix(&self) -> String {
|
||
for c in 'A'..='Z' {
|
||
let s = c.to_string();
|
||
if !self.parents.iter().any(|p| p.parent.as_ref().is_some_and(|i| i.prefix == s)) {
|
||
return s;
|
||
}
|
||
}
|
||
format!("P{}", self.parents.len() + 1)
|
||
}
|
||
|
||
/// 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) {
|
||
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) {
|
||
let cx = it.bounds().center().x;
|
||
let pi = sp.page_at_x(cx).unwrap_or(0);
|
||
let dx = sp.pages[pi].x;
|
||
let mut it = Arc::unwrap_or_clone(it);
|
||
it.xf = Affine::translate((-dx, 0.0)) * it.xf;
|
||
per[pi].push(Arc::new(it));
|
||
}
|
||
for (p, items) in sp.pages.into_iter().zip(per) {
|
||
pages.push((p, items));
|
||
}
|
||
}
|
||
let facing = self.settings.facing_pages;
|
||
let rtl = facing && self.settings.right_to_left_binding;
|
||
let mut spreads: Vec<Spread> = Vec::new();
|
||
let mut cur: Vec<(Page, Vec<Arc<Item>>)> = Vec::new();
|
||
let total = pages.len();
|
||
let flush = |cur: &mut Vec<(Page, Vec<Arc<Item>>)>, spreads: &mut Vec<Spread>, id: SpreadId| {
|
||
if cur.is_empty() {
|
||
return;
|
||
}
|
||
let mut sp = Spread { id, pages: vec![], items: vec![], parent: None, allow_shuffle: true };
|
||
let n = cur.len();
|
||
let mut all_items = Vec::new();
|
||
for (i, (mut p, items)) in cur.drain(..).enumerate() {
|
||
p.side = if !facing {
|
||
PageSide::Single
|
||
} else if n == 1 {
|
||
// Lone page: right if odd page number position, decided by caller via side preset.
|
||
p.side
|
||
} else if (i == 0) != rtl {
|
||
PageSide::Left
|
||
} else {
|
||
PageSide::Right
|
||
};
|
||
all_items.push(items);
|
||
sp.pages.push(p);
|
||
}
|
||
sp.relayout();
|
||
for (p, items) in sp.pages.iter().zip(all_items) {
|
||
for it in items {
|
||
let mut it = Arc::unwrap_or_clone(it);
|
||
it.xf = Affine::translate((p.x, 0.0)) * it.xf;
|
||
sp.items.push(Arc::new(it));
|
||
}
|
||
}
|
||
spreads.push(sp);
|
||
};
|
||
for (i, (mut p, items)) in pages.into_iter().enumerate() {
|
||
if facing {
|
||
// 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 = 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() {
|
||
let id = SpreadId(self.alloc());
|
||
flush(&mut cur, &mut spreads, id);
|
||
}
|
||
cur.push((p, items));
|
||
if cur.len() == 2 || i + 1 == total {
|
||
let id = SpreadId(self.alloc());
|
||
flush(&mut cur, &mut spreads, id);
|
||
}
|
||
} else {
|
||
cur.push((p, items));
|
||
let id = SpreadId(self.alloc());
|
||
flush(&mut cur, &mut spreads, id);
|
||
}
|
||
}
|
||
let mut out: Vec<Arc<Spread>> = spreads.into_iter().map(Arc::new).collect();
|
||
// Re-insert kept (non-shuffling) spreads at roughly their old page positions.
|
||
for (page_pos, sp) in kept.into_iter().rev() {
|
||
let mut n = 0;
|
||
let mut at = out.len();
|
||
for (i, s) in out.iter().enumerate() {
|
||
if n >= page_pos {
|
||
at = i;
|
||
break;
|
||
}
|
||
n += s.pages.len();
|
||
}
|
||
out.insert(at, sp);
|
||
}
|
||
self.spreads = out;
|
||
}
|
||
|
||
/// Insert `count` pages after absolute page `after` (None = at the start), using `parent`.
|
||
pub fn insert_pages(&mut self, after: Option<usize>, count: usize, parent: Option<SpreadId>) -> Result<Vec<PageId>> {
|
||
if count == 0 {
|
||
return Ok(vec![]);
|
||
}
|
||
let n = self.page_count();
|
||
let at = after.map_or(0, |a| a.saturating_add(1).min(n));
|
||
let template = self.page(at.saturating_sub(1).min(n.saturating_sub(1))).cloned();
|
||
let (cols, gutter, margins) =
|
||
template.as_ref().map(|t| (t.columns.count, t.columns.gutter, t.margins)).unwrap_or((1, 12.0, Margins::uniform(36.0)));
|
||
let mut ids = Vec::new();
|
||
let new_pages: Vec<Page> = (0..count)
|
||
.map(|_| {
|
||
let mut p = self.make_page(parent, cols, gutter, margins);
|
||
if let Some(t) = &template {
|
||
p.width = t.width;
|
||
p.height = t.height;
|
||
}
|
||
ids.push(p.id);
|
||
p
|
||
})
|
||
.collect();
|
||
// Splice into the spread holding page `at` (or append a new spread), then repaginate. At
|
||
// the start of a spread that doesn't shuffle, the pages go in before it instead.
|
||
let loc = match self.page_loc(at) {
|
||
Some((si, 0)) if !self.spreads[si].allow_shuffle => {
|
||
let id = SpreadId(self.alloc());
|
||
let mut sp = Spread { id, pages: new_pages, items: vec![], parent: None, allow_shuffle: true };
|
||
sp.relayout();
|
||
self.spreads.insert(si, Arc::new(sp));
|
||
self.repaginate();
|
||
return Ok(ids);
|
||
}
|
||
l => l,
|
||
};
|
||
match loc {
|
||
Some((si, pi)) => {
|
||
let sp = Arc::make_mut(&mut self.spreads[si]);
|
||
let x = sp.pages[pi].x;
|
||
let w: f64 = new_pages.iter().map(|p| p.width).sum();
|
||
// Shift items right of the insertion point.
|
||
for it in &mut sp.items {
|
||
if it.bounds().center().x >= x {
|
||
let it = Arc::make_mut(it);
|
||
it.xf = Affine::translate((w, 0.0)) * it.xf;
|
||
}
|
||
}
|
||
for (k, p) in new_pages.into_iter().enumerate() {
|
||
sp.pages.insert(pi + k, p);
|
||
}
|
||
sp.relayout();
|
||
}
|
||
None => {
|
||
let id = SpreadId(self.alloc());
|
||
let mut sp = Spread { id, pages: new_pages, items: vec![], parent: None, allow_shuffle: true };
|
||
sp.relayout();
|
||
self.spreads.push(Arc::new(sp));
|
||
}
|
||
}
|
||
self.repaginate();
|
||
Ok(ids)
|
||
}
|
||
|
||
/// Delete absolute pages (items whose centre is on a deleted page are deleted too).
|
||
pub fn delete_pages(&mut self, pages: &[usize]) -> Result<()> {
|
||
if pages.len() >= self.page_count() {
|
||
return Err(DocError::Invalid("a document needs at least one page".into()));
|
||
}
|
||
let mut doomed: Vec<(usize, usize)> = pages.iter().filter_map(|p| self.page_loc(*p)).collect();
|
||
doomed.sort();
|
||
doomed.dedup();
|
||
let mut remove_items: Vec<ItemId> = Vec::new();
|
||
for &(si, pi) in &doomed {
|
||
let sp = &self.spreads[si];
|
||
for it in &sp.items {
|
||
if sp.page_at_x(it.bounds().center().x) == Some(pi) {
|
||
remove_items.push(it.id);
|
||
}
|
||
}
|
||
}
|
||
for id in remove_items {
|
||
let _ = self.remove_item(id);
|
||
}
|
||
for &(si, pi) in doomed.iter().rev() {
|
||
let sp = Arc::make_mut(&mut self.spreads[si]);
|
||
let w = sp.pages[pi].width;
|
||
let x = sp.pages[pi].x;
|
||
sp.pages.remove(pi);
|
||
for it in &mut sp.items {
|
||
if it.bounds().center().x >= x + w {
|
||
let it = Arc::make_mut(it);
|
||
it.xf = Affine::translate((-w, 0.0)) * it.xf;
|
||
}
|
||
}
|
||
sp.relayout();
|
||
}
|
||
self.spreads.retain(|s| !s.pages.is_empty());
|
||
self.repaginate();
|
||
Ok(())
|
||
}
|
||
|
||
/// Move absolute page `from` so it ends up at index `to` (items travel with it).
|
||
pub fn move_page(&mut self, from: usize, to: usize) -> Result<()> {
|
||
let n = self.page_count();
|
||
if from >= n || to >= n {
|
||
return Err(DocError::NoPage(from.max(to)));
|
||
}
|
||
if from == to {
|
||
return Ok(());
|
||
}
|
||
// Flatten to single-page spreads, reorder, repaginate.
|
||
let facing = self.settings.facing_pages;
|
||
self.settings.facing_pages = false;
|
||
self.repaginate();
|
||
let sp = self.spreads.remove(from);
|
||
self.spreads.insert(to, sp);
|
||
self.settings.facing_pages = facing;
|
||
self.repaginate();
|
||
Ok(())
|
||
}
|
||
|
||
/// Move absolute page `abs` (with the objects on it) to the end of spread `to`, which stops
|
||
/// shuffling (Layout › Pages: spreads of up to 10 pages).
|
||
pub fn page_to_spread(&mut self, abs: usize, to: usize) -> Result<()> {
|
||
let (si, pi) = self.page_loc(abs).ok_or(DocError::NoPage(abs))?;
|
||
if to >= self.spreads.len() {
|
||
return Err(DocError::Invalid(format!("no spread {to}")));
|
||
}
|
||
if si == to {
|
||
return Ok(());
|
||
}
|
||
if self.spreads[to].pages.len() >= 10 {
|
||
return Err(DocError::Invalid("a spread holds at most 10 pages".into()));
|
||
}
|
||
// Take the page and its objects out of their spread.
|
||
let src = Arc::make_mut(&mut self.spreads[si]);
|
||
let page = src.pages.remove(pi);
|
||
let pb = page.bounds();
|
||
let (moving, staying): (Vec<_>, Vec<_>) = std::mem::take(&mut src.items).into_iter().partition(|it| {
|
||
let c = it.bounds().center();
|
||
c.x >= pb.x0 && c.x < pb.x1
|
||
});
|
||
src.items = staying;
|
||
src.relayout();
|
||
// Append to the target; objects keep their place on the page.
|
||
let dst = Arc::make_mut(&mut self.spreads[to]);
|
||
let x = dst.pages.last().map_or(0.0, |p| p.x + p.width);
|
||
let dx = x - page.x;
|
||
dst.pages.push(page);
|
||
dst.relayout();
|
||
dst.allow_shuffle = false;
|
||
for it in moving {
|
||
let mut it = Arc::unwrap_or_clone(it);
|
||
it.xf = Affine::translate((dx, 0.0)) * it.xf;
|
||
dst.items.push(Arc::new(it));
|
||
}
|
||
if self.spreads[si].pages.is_empty() {
|
||
self.spreads.remove(si);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Apply a parent (None = [None]) to absolute pages.
|
||
pub fn apply_parent(&mut self, pages: &[usize], parent: Option<SpreadId>) -> Result<()> {
|
||
if let Some(p) = parent
|
||
&& self.parent_index(p).is_none()
|
||
{
|
||
return Err(DocError::Invalid(format!("no parent {p}")));
|
||
}
|
||
for &abs in pages {
|
||
let (si, pi) = self.page_loc(abs).ok_or(DocError::NoPage(abs))?;
|
||
Arc::make_mut(&mut self.spreads[si]).pages[pi].parent = parent;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Parent page (spread index, page index) to show behind document page `abs`.
|
||
pub fn parent_page_for(&self, abs: usize) -> Option<(usize, usize)> {
|
||
let page = self.page(abs)?;
|
||
let pi = self.parent_index(page.parent?)?;
|
||
let ps = &self.parents[pi];
|
||
// A parent spread can come without pages from a file.
|
||
// A single-sided page takes the parent's first page, like a left-hand page.
|
||
let first = ps.pages.len() >= 2 && matches!(page.side, PageSide::Left | PageSide::Single);
|
||
let idx = if first { 0 } else { ps.pages.len().checked_sub(1)? };
|
||
Some((pi, idx))
|
||
}
|
||
|
||
/// Ruler guides document page `abs` inherits from its applied parent page. They are stored on
|
||
/// the parent page only: positions are moved from the parent page's place in its spread to
|
||
/// this page's place in its own, so they draw and snap where the page is. Editing them
|
||
/// happens on the parent page.
|
||
pub fn inherited_guides(&self, abs: usize) -> Vec<Guide> {
|
||
let (Some(page), Some((pi, qi))) = (self.page(abs), self.parent_page_for(abs)) else { return Vec::new() };
|
||
let Some(parent) = self.parents.get(pi).and_then(|ps| ps.pages.get(qi)) else { return Vec::new() };
|
||
let dx = page.x - parent.x;
|
||
parent
|
||
.guides
|
||
.iter()
|
||
.map(|g| {
|
||
let mut g = g.clone();
|
||
if g.orientation == Orientation::Vertical {
|
||
g.position += dx;
|
||
}
|
||
g
|
||
})
|
||
.collect()
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn new_facing_document_layout() {
|
||
let d = Document::new(&NewDocument { pages: 4, ..Default::default() });
|
||
assert_eq!(d.page_count(), 4);
|
||
// [1] [2 3] [4]
|
||
assert_eq!(d.spreads.iter().map(|s| s.pages.len()).collect::<Vec<_>>(), vec![1, 2, 1]);
|
||
assert_eq!(d.spreads[0].pages[0].side, PageSide::Right);
|
||
assert_eq!(d.spreads[1].pages[0].side, PageSide::Left);
|
||
assert_eq!(d.spreads[1].pages[1].x, 612.0);
|
||
assert_eq!(d.parents.len(), 1);
|
||
assert_eq!(d.parents[0].pages.len(), 2);
|
||
d.check().unwrap();
|
||
}
|
||
|
||
/// A document whose parent spread has no pages passes `check()`; showing the parent behind a
|
||
/// page used to compute index `0 - 1`.
|
||
#[test]
|
||
fn parent_without_pages_is_not_shown() {
|
||
let mut d = Document::new(&NewDocument { pages: 2, ..Default::default() });
|
||
assert!(d.parent_page_for(0).is_some());
|
||
Arc::make_mut(&mut d.parents[0]).pages.clear();
|
||
d.check().unwrap();
|
||
assert_eq!(d.parent_page_for(0), None);
|
||
assert_eq!(d.parent_page_for(1), None);
|
||
}
|
||
|
||
/// A single-sided page applying a two-page parent shows the parent's first page.
|
||
#[test]
|
||
fn single_sided_page_shows_first_parent_page() {
|
||
let mut d = Document::new(&NewDocument { pages: 2, facing_pages: false, ..Default::default() });
|
||
let extra = d.parents[0].pages[0].clone();
|
||
Arc::make_mut(&mut d.parents[0]).pages.push(extra);
|
||
assert_eq!(d.parents[0].pages.len(), 2);
|
||
assert_eq!(d.parent_page_for(0), Some((0, 0)));
|
||
assert_eq!(d.parent_page_for(1), Some((0, 0)));
|
||
Arc::make_mut(&mut d.parents[0]).pages.clear();
|
||
assert_eq!(d.parent_page_for(0), None);
|
||
}
|
||
|
||
#[test]
|
||
fn non_facing_one_page_per_spread() {
|
||
let d = Document::new(&NewDocument { pages: 3, facing_pages: false, ..Default::default() });
|
||
assert_eq!(d.spreads.len(), 3);
|
||
assert!(d.spreads.iter().all(|s| s.pages.len() == 1 && s.pages[0].side == PageSide::Single));
|
||
}
|
||
|
||
#[test]
|
||
fn insert_and_delete_pages_reshuffle() {
|
||
let mut d = Document::new(&NewDocument { pages: 1, ..Default::default() });
|
||
d.insert_pages(Some(0), 3, d.parents.first().map(|p| p.id)).unwrap();
|
||
assert_eq!(d.page_count(), 4);
|
||
assert_eq!(d.spreads.len(), 3);
|
||
d.delete_pages(&[1, 2]).unwrap();
|
||
assert_eq!(d.page_count(), 2);
|
||
assert_eq!(d.spreads.iter().map(|s| s.pages.len()).collect::<Vec<_>>(), vec![1, 1]);
|
||
assert!(d.delete_pages(&[0, 1]).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn items_travel_with_pages() {
|
||
let mut d = Document::new(&NewDocument { pages: 3, ..Default::default() });
|
||
// Put a frame on page 3 (spread 1, page index 1).
|
||
let lid = d.default_layer();
|
||
let r = designcraft_geom::Rect::new(612.0 + 100.0, 100.0, 612.0 + 200.0, 200.0);
|
||
let (id, _) = d.add_text_frame(crate::SpreadRef::Doc(1), r, lid, "hello", Default::default()).unwrap();
|
||
d.move_page(2, 0).unwrap();
|
||
// The frame's page is now page 1 (spread 0, single right page at x=0).
|
||
let loc = d.find(id).unwrap();
|
||
assert_eq!(loc.spread, crate::SpreadRef::Doc(0));
|
||
let b = d.item(id).unwrap().bounds();
|
||
assert!((b.x0 - 100.0).abs() < 1e-9, "{b:?}");
|
||
d.check().unwrap();
|
||
}
|
||
|
||
#[test]
|
||
fn presets_and_parents() {
|
||
let nd = NewDocument::from_preset("A4").unwrap();
|
||
assert!((nd.width - 595.2756).abs() < 1e-3);
|
||
let mut d = Document::new(&nd);
|
||
assert_eq!(d.next_parent_prefix(), "B");
|
||
let b = d.add_parent("B", "Parent", 2, 12.0, Margins::uniform(36.0));
|
||
d.apply_parent(&[0], Some(b)).unwrap();
|
||
assert_eq!(d.parent_page_for(0), Some((1, 1)));
|
||
d.apply_parent(&[0], None).unwrap();
|
||
assert_eq!(d.parent_page_for(0), None);
|
||
}
|
||
}
|