Files
vectorcraft/crates/tools/src/printtiling.rs
T
Michael GiesonandClaude Opus 5.5 2d0da1472f Cursors: 36 more tool states get cursors of their own, listed in cursors.toml
Node editing as the public manual describes it: Direct Selection's anchor squares (hollow
or filled), Reshape Segment over a curved segment, Group Selection's plus (also Direct
Selection with Alt), the arrow hollow while a drag snaps, and Alt-copy arrowheads. The
Anchor Point caret, the Curvature pen and Free Transform's side-shear arrows (ShearH,
ShearV) are drawn in code. Lasso, the freehand, symbolism and other tools return cursors
of their own that borrow their old look until drawn (Cursor::stand_in).

crates/tools/src/cursors.toml lists every Cursor value, when it shows and its hotspot,
for whoever draws them; `cargo xtask cursors` (in ci) warns about a missing, unknown,
repeated or incomplete entry and never fails. The busy cursor is listed as missing (G1).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 12:26:12 +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::PrintTiling
}
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");
}
}