mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 19:26:46 -06:00
108 lines
3.6 KiB
Rust
108 lines
3.6 KiB
Rust
//! 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<Overlay>,
|
|
}
|
|
|
|
#[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<Target>, Vec<Target>) {
|
|
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<Target>, 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<Overlay>) {
|
|
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 } }
|
|
}
|