//! The Print Tiling tool: drag to put the printed pages where they print. The pointer is the //! top-left corner of the first page's imageable area (snapping to the artboard's edges); the //! drag previews `print.tiling.set` and commits it as one undo step. A double click puts the pages //! back where the placement puts them (`print.tiling.set {reset}`). The canvas shows the pages //! while the tool is active (as View → Show Print Tiling does). use serde_json::json; use vectorcraft_geom::{Point, Rect}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; /// Create the Print Tiling tool by id (None = not ours). pub fn create(id: &str) -> Option> { (id == "printTiling").then(|| Box::new(PrintTilingTool::default()) as Box) } #[derive(Default)] pub struct PrintTilingTool { /// The artboard the drag started on (index and rect), while dragging. drag: Option<(Option, Option)>, began: bool, last: Point, } /// `v` snapped to the nearest of `edges` within `tol`. fn snap1(v: f64, edges: [f64; 2], tol: f64) -> f64 { edges.into_iter().filter(|e| (e - v).abs() <= tol).min_by(|a, b| (a - v).abs().total_cmp(&(b - v).abs())).unwrap_or(v) } impl Tool for PrintTilingTool { fn id(&self) -> &'static str { "printTiling" } fn busy(&self) -> bool { self.drag.is_some() } fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { match ev.kind { PointerKind::Down => { let hit = cx.doc.artboards.iter().position(|a| a.rect.contains(ev.pos)); self.drag = Some((hit, hit.and_then(|i| cx.doc.artboards.get(i)).map(|a| a.rect))); (self.began, self.last) = (false, ev.pos); vec![] } PointerKind::Drag => { let Some((artboard, rect)) = self.drag else { return vec![] }; let tol = cx.tol(5.0); let p = match rect { Some(r) => Point::new(snap1(ev.pos.x, [r.x0, r.x1], tol), snap1(ev.pos.y, [r.y0, r.y1], tol)), None => ev.pos, }; self.last = p; let mut out = vec![]; if !self.began { self.began = true; out.push(Action::Begin("Print Tiling".into())); } out.push(Action::Preview("print.tiling.set".into(), json!({ "origin": [p.x, p.y], "artboard": artboard }))); out } PointerKind::Up => { let began = std::mem::take(&mut self.began); if self.drag.take().is_some() && began { vec![Action::Commit] } else { vec![] } } PointerKind::DoubleClick => { self.drag = None; vec![Action::Exec("print.tiling.set".into(), json!({ "reset": true }))] } PointerKind::Move => vec![], } } fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec { if key == ToolKey::Escape { self.deactivate(cx) } else { vec![] } } fn overlays(&self, cx: &ToolContext) -> Vec { match self.drag { // How far the corner is from the artboard's. Some((_, Some(r))) if self.began => { vec![Overlay::Measure { p: self.last, text: cx.offset_label(self.last.x - r.x0, self.last.y - r.y0) }] } _ => vec![], } } fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor { Cursor::Crosshair } fn deactivate(&mut self, _cx: &ToolContext) -> Vec { self.drag = None; if std::mem::take(&mut self.began) { vec![Action::Cancel] } else { vec![] } } } #[cfg(test)] mod tests { use serde_json::json; use vectorcraft_doc::Selection; use super::*; use crate::testutil::{cx, doc_with_rect, paint}; fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent { PointerEvent::new(kind, x, y) } #[test] fn a_drag_previews_the_origin_and_commits_once() { let (d, _) = doc_with_rect(); let (s, pd) = (Selection::default(), paint()); let c = cx(&d, &s, &pd); let mut t = PrintTilingTool::default(); assert!(t.pointer(&c, &ev(PointerKind::Down, 50.0, 60.0)).is_empty()); let a = t.pointer(&c, &ev(PointerKind::Drag, 40.0, 30.0)); assert_eq!(a.first(), Some(&Action::Begin("Print Tiling".into()))); assert_eq!(a.get(1), Some(&Action::Preview("print.tiling.set".into(), json!({"origin": [40.0, 30.0], "artboard": 0})))); // Near the artboard's edge the corner snaps to it. let a = t.pointer(&c, &ev(PointerKind::Drag, 2.0, 497.0)); assert_eq!(a, vec![Action::Preview("print.tiling.set".into(), json!({"origin": [0.0, 500.0], "artboard": 0}))]); assert!(matches!(t.overlays(&c).as_slice(), [Overlay::Measure { .. }])); assert_eq!(t.pointer(&c, &ev(PointerKind::Up, 2.0, 497.0)), vec![Action::Commit]); assert!(!t.busy()); // A click alone changes nothing; a double click resets. t.pointer(&c, &ev(PointerKind::Down, 10.0, 10.0)); assert!(t.pointer(&c, &ev(PointerKind::Up, 10.0, 10.0)).is_empty()); assert_eq!(t.pointer(&c, &ev(PointerKind::DoubleClick, 10.0, 10.0)), vec![Action::Exec("print.tiling.set".into(), json!({"reset": true}))]); // Escape mid-drag cancels. t.pointer(&c, &ev(PointerKind::Down, 10.0, 10.0)); t.pointer(&c, &ev(PointerKind::Drag, 20.0, 20.0)); assert_eq!(t.key(&c, ToolKey::Escape, Mods::default()), vec![Action::Cancel]); assert_eq!(crate::create("printTiling").id(), "printTiling"); } }