Files
vectorcraft/crates/tools/src/printtiling.rs
T
bflatasticandClaude Opus 5.5 1b9b37454b M4.80: Print Tiling tool and View > Show Print Tiling
View > Show Print Tiling (view.printTiling) draws the pages of the
document's print settings on the canvas, dashed and numbered, from
print.preview's own layout (print.tiling), so the canvas and the
printed job agree. The Print Tiling tool replaces the no-op tool: a
drag places the first page's imageable corner (print.tiling.set,
saved as print_setup.tileOrigin, one undo step); a double click on
the canvas or the tool button resets it. Tiles start at the origin
and extend to cover the art; the Print dialog shows the tool's
placement with a Reset button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 17:26:59 +07:00

138 lines
5.7 KiB
Rust

//! 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<Box<dyn Tool>> {
(id == "printTiling").then(|| Box::new(PrintTilingTool::default()) as Box<dyn Tool>)
}
#[derive(Default)]
pub struct PrintTilingTool {
/// The artboard the drag started on (index and rect), while dragging.
drag: Option<(Option<usize>, Option<Rect>)>,
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<Action> {
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<Action> {
if key == ToolKey::Escape { self.deactivate(cx) } else { vec![] }
}
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
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<Action> {
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");
}
}