//! Snapping and smart guides: page edges and centres, margins, column guides, ruler guides and //! other objects' edges/centres on the same spread. Returns the snap correction and the guide //! lines to draw (canvas coordinates). use designcraft_doc::{ItemId, Orientation, SpreadRef}; use designcraft_geom::{Point, Rect, Vec2}; use crate::{Overlay, ToolContext}; #[derive(Clone, Debug, Default)] pub struct Snap { pub delta: Vec2, pub guides: Vec, } #[derive(Clone, Copy)] struct Target { v: f64, /// Extent of the target along the other axis (for drawing the guide). lo: f64, hi: f64, smart: bool, } fn targets(cx: &ToolContext, sr: SpreadRef, exclude: &[ItemId]) -> (Vec, Vec) { let mut xs = Vec::new(); let mut ys = Vec::new(); let Some(sp) = cx.doc.spread(sr) else { return (xs, ys) }; let push = |v: &mut Vec, val: f64, lo: f64, hi: f64, smart: bool| v.push(Target { v: val, lo, hi, smart }); for p in &sp.pages { let b = p.bounds(); let m = p.margin_rect(); for x in [b.x0, b.center().x, b.x1, m.x0, m.x1] { push(&mut xs, x, b.y0, b.y1, false); } for y in [b.y0, b.center().y, b.y1, m.y0, m.y1] { push(&mut ys, y, b.x0, b.x1, false); } for c in p.column_rects() { push(&mut xs, c.x0, b.y0, b.y1, false); push(&mut xs, c.x1, b.y0, b.y1, false); } for g in &p.guides { match g.orientation { Orientation::Vertical => push(&mut xs, g.position, b.y0, b.y1, false), Orientation::Horizontal => push(&mut ys, g.position, b.x0, b.x1, false), } } } for it in &sp.items { if exclude.contains(&it.id) || it.hidden { continue; } let b = it.bounds(); for x in [b.x0, b.center().x, b.x1] { push(&mut xs, x, b.y0, b.y1, true); } for y in [b.y0, b.center().y, b.y1] { push(&mut ys, y, b.x0, b.x1, true); } } (xs, ys) } fn best(cands: &[f64], targets: &[Target], tol: f64) -> Option<(f64, Target)> { let mut out: Option<(f64, Target)> = None; for &c in cands { for t in targets { let d = t.v - c; if d.abs() <= tol && out.is_none_or(|(od, _)| d.abs() < od.abs()) { out = Some((d, *t)); } } } out } /// Snap a moving rect (spread coords). Edges and centre snap independently per axis. pub fn snap_rect(cx: &ToolContext, sr: SpreadRef, r: Rect, exclude: &[ItemId]) -> Snap { let tol = cx.tol(5.0); let (xs, ys) = targets(cx, sr, exclude); let mut s = Snap::default(); let off = cx.layout.offset(sr); if let Some((d, t)) = best(&[r.x0, r.center().x, r.x1], &xs, tol) { s.delta.x = d; let lo = t.lo.min(r.y0) - 6.0; let hi = t.hi.max(r.y1) + 6.0; s.guides.push(guide(Point::new(t.v, lo) + off, Point::new(t.v, hi) + off, t.smart)); } if let Some((d, t)) = best(&[r.y0, r.center().y, r.y1], &ys, tol) { s.delta.y = d; let lo = t.lo.min(r.x0) - 6.0; let hi = t.hi.max(r.x1) + 6.0; s.guides.push(guide(Point::new(lo, t.v) + off, Point::new(hi, t.v) + off, t.smart)); } s } /// Snap a single point (drawing tools). pub fn snap_point(cx: &ToolContext, sr: SpreadRef, p: Point) -> (Point, Vec) { let s = snap_rect(cx, sr, Rect::from_points(p, p), &[]); (p + s.delta, s.guides) } fn guide(a: Point, b: Point, smart: bool) -> Overlay { if smart { Overlay::Guide { a, b } } else { Overlay::Line { a, b, color: [255, 0, 255], dashed: false } } }