mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
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>
138 lines
5.7 KiB
Rust
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");
|
|
}
|
|
}
|