mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 03:06:56 -06:00
141 lines
5.2 KiB
Rust
141 lines
5.2 KiB
Rust
//! Where spreads sit on the canvas (the pasteboard): stacked vertically, spines aligned at x = 0,
|
||
//! parents shown on their own canvas when a parent spread is being edited.
|
||
|
||
use designcraft_doc::{Document, SpreadRef};
|
||
use designcraft_geom::{Affine, Point, Rect, Vec2};
|
||
use serde::Serialize;
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
|
||
pub struct SpreadSlot {
|
||
pub spread: SpreadRef,
|
||
/// Spread → canvas: a translation, turned in quarter turns for a rotated spread view.
|
||
#[serde(skip)]
|
||
pub xf: Affine,
|
||
/// Quarter turns clockwise (View › Rotate Spread).
|
||
pub rotation: u8,
|
||
/// Spread page bounds in canvas coordinates.
|
||
pub bounds: Rect,
|
||
}
|
||
|
||
impl SpreadSlot {
|
||
pub fn to_canvas(&self, p: Point) -> Point {
|
||
self.xf * p
|
||
}
|
||
pub fn to_spread(&self, p: Point) -> Point {
|
||
self.xf.inverse() * p
|
||
}
|
||
/// A canvas movement as a spread movement.
|
||
pub fn delta_to_spread(&self, d: Vec2) -> Vec2 {
|
||
let m = self.xf.inverse();
|
||
(m * Point::new(d.x, d.y)) - (m * Point::ORIGIN)
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Default, PartialEq, Serialize)]
|
||
pub struct CanvasLayout {
|
||
pub slots: Vec<SpreadSlot>,
|
||
/// Pasteboard extent (canvas coordinates).
|
||
pub pasteboard: Rect,
|
||
}
|
||
|
||
/// Vertical gap between spreads (points).
|
||
pub const SPREAD_GAP: f64 = 72.0;
|
||
|
||
impl CanvasLayout {
|
||
/// Layout of the document spreads (or the parent spreads when `parents` is true).
|
||
pub fn new(doc: &Document, parents: bool) -> Self {
|
||
let list: Vec<SpreadRef> =
|
||
if parents { (0..doc.parents.len()).map(SpreadRef::Parent).collect() } else { (0..doc.spreads.len()).map(SpreadRef::Doc).collect() };
|
||
let mut slots = Vec::with_capacity(list.len());
|
||
let mut y = 0.0;
|
||
let mut pb: Option<Rect> = None;
|
||
let (px, py) = doc.settings.pasteboard;
|
||
for r in list {
|
||
let Some(sp) = doc.spread(r) else { continue };
|
||
let b = sp.bounds();
|
||
let rotation = sp.pages.first().map_or(0, |p| p.view_rotation % 4);
|
||
// Turned about the spread's centre, then placed: centred where the unturned spread
|
||
// would be, its top at `y`.
|
||
let turn = Affine::rotate_about(rotation as f64 * std::f64::consts::FRAC_PI_2, b.center());
|
||
let tb = turn.transform_rect_bbox(b);
|
||
let cx = b.center().x - sp.spine_x();
|
||
let xf = Affine::translate((cx - tb.center().x, y - tb.y0)) * turn;
|
||
let cb = xf.transform_rect_bbox(b);
|
||
let w = cb.width();
|
||
let pr = Rect::new(cb.x0 - px.max(w), cb.y0 - py * 0.5, cb.x1 + px.max(w), cb.y1 + py * 0.5);
|
||
pb = Some(pb.map_or(pr, |p| p.union(pr)));
|
||
slots.push(SpreadSlot { spread: r, xf, rotation, bounds: cb });
|
||
y += cb.height() + SPREAD_GAP;
|
||
}
|
||
let pasteboard = pb.unwrap_or(Rect::new(-500.0, -500.0, 500.0, 500.0)).inflate(0.0, py * 0.5);
|
||
CanvasLayout { slots, pasteboard }
|
||
}
|
||
|
||
/// The spread under (or nearest to) a canvas point, and the point in spread coordinates.
|
||
pub fn spread_at(&self, p: Point) -> Option<(SpreadRef, Point)> {
|
||
let s = self.slots.iter().min_by(|a, b| dist_y(a.bounds, p.y).total_cmp(&dist_y(b.bounds, p.y)))?;
|
||
Some((s.spread, s.to_spread(p)))
|
||
}
|
||
|
||
pub fn slot(&self, r: SpreadRef) -> Option<&SpreadSlot> {
|
||
self.slots.iter().find(|s| s.spread == r)
|
||
}
|
||
|
||
/// Spread → canvas transform of spread `r`.
|
||
pub fn xf(&self, r: SpreadRef) -> Affine {
|
||
self.slot(r).map(|s| s.xf).unwrap_or(Affine::IDENTITY)
|
||
}
|
||
|
||
pub fn to_canvas(&self, r: SpreadRef, p: Point) -> Point {
|
||
self.xf(r) * p
|
||
}
|
||
|
||
pub fn to_spread(&self, r: SpreadRef, p: Point) -> Point {
|
||
self.xf(r).inverse() * p
|
||
}
|
||
|
||
/// A canvas movement as a movement in spread `r`.
|
||
pub fn delta_to_spread(&self, r: SpreadRef, d: Vec2) -> Vec2 {
|
||
let m = self.xf(r).inverse();
|
||
(m * Point::new(d.x, d.y)) - (m * Point::ORIGIN)
|
||
}
|
||
|
||
/// Canvas rect of a document page.
|
||
pub fn page_rect(&self, doc: &Document, abs: usize) -> Option<Rect> {
|
||
let (si, pi) = doc.page_loc(abs)?;
|
||
Some(self.xf(SpreadRef::Doc(si)).transform_rect_bbox(doc.spreads[si].pages[pi].bounds()))
|
||
}
|
||
}
|
||
|
||
fn dist_y(r: Rect, y: f64) -> f64 {
|
||
if y < r.y0 - SPREAD_GAP / 2.0 {
|
||
r.y0 - y
|
||
} else if y > r.y1 + SPREAD_GAP / 2.0 {
|
||
y - r.y1
|
||
} else {
|
||
0.0
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use designcraft_doc::build::NewDocument;
|
||
|
||
#[test]
|
||
fn spreads_stack_on_the_spine() {
|
||
let d = Document::new(&NewDocument { pages: 3, ..Default::default() });
|
||
let l = CanvasLayout::new(&d, false);
|
||
assert_eq!(l.slots.len(), 2);
|
||
// Page 1 is a right page: it starts at the spine (x = 0).
|
||
assert_eq!(l.slots[0].bounds.x0, 0.0);
|
||
// Spread 2 (pages 2–3) straddles the spine.
|
||
assert_eq!(l.slots[1].bounds.x0, -612.0);
|
||
assert_eq!(l.slots[1].bounds.y0, 792.0 + SPREAD_GAP);
|
||
let (r, p) = l.spread_at(Point::new(-100.0, 900.0)).unwrap();
|
||
assert_eq!(r, SpreadRef::Doc(1));
|
||
assert_eq!(p, Point::new(512.0, 900.0 - 792.0 - SPREAD_GAP));
|
||
assert_eq!(l.page_rect(&d, 2).unwrap().x0, 0.0);
|
||
}
|
||
}
|