diff --git a/crates/engine/src/cmd/printtiling.rs b/crates/engine/src/cmd/printtiling.rs index c4576985f..fd9f69d51 100644 --- a/crates/engine/src/cmd/printtiling.rs +++ b/crates/engine/src/cmd/printtiling.rs @@ -5,7 +5,7 @@ use serde_json::{Value, json}; use vectorcraft_doc::Document; -use vectorcraft_geom::Point; +use vectorcraft_geom::{Point, Rect}; use vectorcraft_pdf::{TileOrigin, TilingPage}; use super::fileio::pdf::pdf_error; @@ -30,7 +30,7 @@ pub fn specs() -> Vec { "Print Tiling", [], None, - "{origin: [x, y] (document point: the top-left corner of the first page's imageable area), artboard? (0-based: the artboard it is measured from; default the printed artboard containing origin, else the first; ignored with artboards: ignore)} | {reset: true} place the printed pages as the Print Tiling tool does, saved with the print settings (tileOrigin: {placed, x, y}, from the artboard's top-left corner; it replaces the placement, and tiles start there), as one undo step; reset → the placement places them again → {tileOrigin}", + "{origin: [x, y] (document point: the top-left corner of the first page's imageable area), artboard? (0-based: the artboard it is measured from; default the printed artboard containing origin, its edges included, else the first; ignored with artboards: ignore)} | {reset: true} place the printed pages as the Print Tiling tool does, saved with the print settings (tileOrigin: {placed, x, y}, from the artboard's top-left corner; it replaces the placement, and tiles start there), as one undo step; reset → the placement places them again → {tileOrigin}", has_doc, set_origin ), @@ -77,6 +77,17 @@ fn point(p: &Value, key: &str) -> Option { Some(Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?)) } +/// The first of the print `regions` holding `at`: one that holds it inside or on its left or top +/// edge, else one that holds it on its right or bottom edge or within a millionth of a point +/// outside an edge (the edges `Document::art_on_artboard` counts). +fn region_holding(regions: &[(Option, Rect)], at: Point) -> Option<&(Option, Rect)> { + let holds = |r: &Rect| { + let r = r.inflate(1e-6, 1e-6); + r.x0 <= at.x && at.x <= r.x1 && r.y0 <= at.y && at.y <= r.y1 + }; + regions.iter().find(|(_, r)| r.contains(at)).or_else(|| regions.iter().find(|(_, r)| holds(r))) +} + fn set_origin(s: &mut Session, p: &Value) -> Result { let doc = &s.doc()?.doc; let current = settings(SET, doc, &json!({}))?; @@ -92,7 +103,7 @@ fn set_origin(s: &mut Session, p: &Value) -> Result { let i = v.as_u64().ok_or_else(|| bad(SET, "artboard must be a 0-based index"))? as usize; regions.iter().find(|(a, _)| a.is_none() || *a == Some(i)).ok_or_else(|| bad(SET, format!("artboard {i} doesn't print")))? } - None => regions.iter().find(|(_, r)| r.contains(at)).or(regions.first()).ok_or_else(|| bad(SET, "nothing prints"))?, + None => region_holding(®ions, at).or(regions.first()).ok_or_else(|| bad(SET, "nothing prints"))?, }; let corner = picked.1.origin(); TileOrigin { placed: true, x: at.x - corner.x, y: at.y - corner.y } diff --git a/crates/engine/src/tests_printtiling.rs b/crates/engine/src/tests_printtiling.rs index 3919e0ded..122acb3ab 100644 --- a/crates/engine/src/tests_printtiling.rs +++ b/crates/engine/src/tests_printtiling.rs @@ -147,6 +147,26 @@ fn tiles_start_at_the_tile_origin_and_cover_the_art() { assert!(s.execute("print.tiling.set", &json!({"origin": [1e9, 2]})).is_err()); } +#[test] +fn an_origin_on_an_artboards_right_or_bottom_edge_is_measured_from_that_artboard() { + // Artboard 0 is (0, 0) to (300, 200). Artboard 1 is (300, 0) to (600, 200), and its left edge is artboard 0's right edge. + let mut s = session(); + s.execute("artboard.new", &json!({"x": 300, "y": 0, "width": 300, "height": 200})).unwrap(); + let mut placed = |x: f64, y: f64| { + let o = s.execute("print.tiling.set", &json!({"origin": [x, y]})).unwrap()["tileOrigin"].clone(); + (o["x"].as_f64().unwrap(), o["y"].as_f64().unwrap()) + }; + assert_eq!(placed(600.0, 50.0), (300.0, 50.0), "artboard 1's right edge"); + assert_eq!(placed(450.0, 200.0), (150.0, 200.0), "artboard 1's bottom edge"); + assert_eq!(placed(600.0, 200.0), (300.0, 200.0), "artboard 1's bottom right corner"); + assert_eq!(placed(150.0, 200.0), (150.0, 200.0), "artboard 0's bottom edge"); + assert_eq!(placed(300.0, 50.0), (0.0, 50.0), "the shared edge: the artboard to its right"); + let (x, _) = placed(600.0 + 1e-9, 50.0); + assert!((x - 300.0).abs() < 1e-6, "roundoff past the edge: {x}"); + // Beyond every artboard the origin is measured from the first. + assert_eq!(placed(600.001, 50.0), (600.001, 50.0), "beyond the edge"); +} + #[test] fn settings_saved_before_the_tile_origin_still_load() { let mut s = session(); diff --git a/crates/tools/src/printtiling.rs b/crates/tools/src/printtiling.rs index ac1a1076a..ba9116b84 100644 --- a/crates/tools/src/printtiling.rs +++ b/crates/tools/src/printtiling.rs @@ -27,6 +27,18 @@ 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) } +/// Index of the first artboard holding `p`: one that holds it inside or on its left or top edge, +/// else one that holds it on its right or bottom edge or within a millionth of a point outside an +/// edge (the edges `Document::art_on_artboard` counts). +fn artboard_holding(cx: &ToolContext, p: Point) -> Option { + let rects = || cx.doc.artboards.iter().map(|a| a.rect); + let holds = |r: Rect| { + let r = r.inflate(1e-6, 1e-6); + r.x0 <= p.x && p.x <= r.x1 && r.y0 <= p.y && p.y <= r.y1 + }; + rects().position(|r| r.contains(p)).or_else(|| rects().position(holds)) +} + impl Tool for PrintTilingTool { fn id(&self) -> &'static str { "printTiling" @@ -39,7 +51,7 @@ impl Tool for PrintTilingTool { 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)); + let hit = artboard_holding(cx, 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![] @@ -98,7 +110,7 @@ impl Tool for PrintTilingTool { #[cfg(test)] mod tests { - use serde_json::json; + use serde_json::{Value, json}; use vectorcraft_doc::Selection; use super::*; @@ -134,4 +146,31 @@ mod tests { assert_eq!(t.key(&c, ToolKey::Escape, Mods::default()), vec![Action::Cancel]); assert_eq!(crate::create("printTiling").id(), "printTiling"); } + + #[test] + fn a_press_on_an_artboards_right_or_bottom_edge_drags_from_that_artboard() { + // Artboard 0 is (0, 0) to (500, 500). Artboard 1 is (500, 0) to (800, 200), and its left edge lies on artboard 0's right edge. + let (mut d, _) = doc_with_rect(); + let mut ab = d.artboards[0].clone(); + ab.rect = Rect::new(500.0, 0.0, 800.0, 200.0); + d.artboards.push(ab); + let (s, pd) = (Selection::default(), paint()); + let c = cx(&d, &s, &pd); + // What a drag to 3 pt up and left of a press at (x, y) previews: the corner snaps to the + // edges of the artboard the press picked. + let drag = |x: f64, y: f64| { + let mut t = PrintTilingTool::default(); + t.pointer(&c, &ev(PointerKind::Down, x, y)); + t.pointer(&c, &ev(PointerKind::Drag, x - 3.0, y - 3.0)).pop() + }; + let preview = + |origin: [f64; 2], artboard: Value| Some(Action::Preview("print.tiling.set".into(), json!({ "origin": origin, "artboard": artboard }))); + assert_eq!(drag(800.0, 100.0), preview([800.0, 97.0], json!(1)), "artboard 1's right edge"); + assert_eq!(drag(650.0, 200.0), preview([647.0, 200.0], json!(1)), "artboard 1's bottom edge"); + assert_eq!(drag(800.0, 200.0), preview([800.0, 200.0], json!(1)), "artboard 1's bottom right corner"); + assert_eq!(drag(300.0, 500.0), preview([297.0, 500.0], json!(0)), "artboard 0's bottom edge"); + assert_eq!(drag(500.0, 100.0), preview([500.0, 97.0], json!(1)), "the shared edge: the artboard to its right"); + assert_eq!(drag(800.0 + 1e-9, 100.0), preview([800.0, 97.0], json!(1)), "roundoff past the edge"); + assert_eq!(drag(800.001, 100.0), preview([800.001 - 3.0, 97.0], Value::Null), "beyond the edge"); + } }