diff --git a/crates/tools/src/guides.rs b/crates/tools/src/guides.rs index e05f7dceb..e793f0424 100644 --- a/crates/tools/src/guides.rs +++ b/crates/tools/src/guides.rs @@ -20,6 +20,18 @@ use vectorcraft_text::{FontDb, layout as layout_text}; use crate::bbox::Handle; use crate::{Overlay, ScreenFrame, ToolContext}; +mod snap; +mod targets; + +pub(crate) use snap::hold_on_path; +pub use snap::{ + DEFAULT_CONSTRUCTION_ANGLES, DrawSnap, HandleSnap, Leave, PointSnap, construction_angles, distance_guides, snap_draw, snap_pick, square, +}; +use targets::Kind; +pub use targets::Targets; +#[cfg(test)] +use targets::{lines_near, object_boxes}; + pub const MAGENTA: [u8; 3] = [0xff, 0x3d, 0xfc]; /// How Smart Guides look (Preferences › Smart Guides › Display Options): their colour, and whether @@ -46,57 +58,6 @@ impl GuideStyle { } } -#[derive(Clone, Copy, Debug, PartialEq)] -enum Kind { - Anchor, - Center, - Edge, - Artboard, - Bleed, - Guide, - Glyph, -} - -impl Kind { - fn label(self) -> &'static str { - match self { - Kind::Anchor => "anchor", - Kind::Center => "center", - Kind::Edge => "path", - Kind::Artboard => "artboard", - Kind::Bleed => "bleed", - Kind::Guide => "guide", - Kind::Glyph => "glyph", - } - } -} - -/// Snap targets gathered from the document (excluding the objects being edited). -#[derive(Default)] -pub struct Targets { - points: Vec<(Point, Kind)>, - xs: Vec<(f64, Point, Kind)>, - ys: Vec<(f64, Point, Kind)>, - /// Path segments and their control bounds, which a point lands on ("path"): gathered only for - /// dragged anchors ([`Self::for_anchor_drag`]). - segments: Vec<(Rect, PathSeg)>, - /// Ruler guides, which pull a point into line where they run (Snap to Point). - rulers: Vec, - /// The bounds of the objects a moved selection spaces itself evenly with (Spacing Guides): - /// gathered only for moves ([`Self::for_move`]). - boxes: Vec, - /// Line Extension: the lines a single dragged anchor keeps to, along the straight segments - /// it ends, as the end that stays put and the unit direction towards the dragged one - /// ([`Self::for_anchor_drag`]). - extensions: Vec<(Point, Vec2)>, - /// Snap to Tangent and Snap to Perpendicular: a drawn line from an anchor lands where it - /// grazes a curve, or meets a segment square ([`Self::touching`]). - tangent: bool, - perpendicular: bool, - /// How the guides these targets produce look. - style: GuideStyle, -} - /// A ruler guide as a snap target. struct Ruler { vertical: bool, @@ -121,1501 +82,5 @@ impl Ruler { } } -/// How many anchors, and how many segments, the targets gather at most. -const BUDGET: usize = 20_000; - -/// What [`Targets::collect_inner`] gathers from each path besides its anchors and bounds. -#[derive(Clone, Copy, PartialEq)] -enum Gather { - /// Nothing more: a moved or resized selection lines up with bounds. - Bounds, - /// Its segments, which a dragged anchor lands on. - Segments, - /// Its segments, and its anchors as lines too: a drawn point lines up with them. - Drawing, -} - -/// The entries of `sorted` (ascending by `key`) whose key is within `tol` of `v`, found by -/// bisection. -fn within(sorted: &[T], key: impl Fn(&T) -> f64, v: f64, tol: f64) -> &[T] { - let lo = sorted.partition_point(|e| key(e) < v - tol); - let hi = sorted.partition_point(|e| key(e) <= v + tol); - sorted.get(lo..hi).unwrap_or_default() -} - -/// Visit `nodes` and everything inside them, but the nodes in `skip` and everything inside those: -/// one pass, however many are skipped. A Shaper Group's original shapes, its first child, are -/// not shown and are skipped too. -fn walk_but<'a>(nodes: &'a [Arc], skip: &HashSet, f: &mut impl FnMut(&'a Node)) { - for n in nodes.iter().filter(|n| !skip.contains(&n.id)) { - f(n); - if let Some(ch) = n.children() { - walk_but(ch.get(usize::from(n.extras().shaper.is_some())..).unwrap_or_default(), skip, f); - } - } -} - -/// The lines of `lines` (sorted by where they run) within `tol` of `v`. -fn lines_near(lines: &[(f64, Point, Kind)], v: f64, tol: f64) -> &[(f64, Point, Kind)] { - within(lines, |l| l.0, v, tol) -} - -/// `r`'s extent along the x axis (`x`) or the y axis. -fn span(r: &Rect, x: bool) -> (f64, f64) { - if x { (r.x0, r.x1) } else { (r.y0, r.y1) } -} - -/// Along axis `x`, does dragging `handle` move the box's minimum edge (true) or maximum (false)? -fn resize_moves_min_edge(handle: Handle, x: bool) -> Option { - let (ax, ay) = handle.axes(); - if x && !ax || !x && !ay { - return None; - } - if x { - Some(matches!(handle, Handle::Left | Handle::TopLeft | Handle::BottomLeft)) - } else { - Some(matches!(handle, Handle::Top | Handle::TopLeft | Handle::TopRight)) - } -} - -/// The objects a moved selection (`moving`) spaces itself evenly with, by their bounds: each -/// visible object on a layer as a whole (a group too), and inside a group holding some of the -/// moving objects its other objects, but a Shaper Group's original shapes. At most [`BUDGET`]. -fn object_boxes(doc: &Document, moving: &[NodeId]) -> Vec { - fn push(nodes: &[Arc], moving: &HashSet, holders: &HashSet, out: &mut Vec) { - for n in nodes.iter().filter(|n| n.visible && !moving.contains(&n.id)) { - if out.len() >= BUDGET { - return; - } - match n.children() { - Some(ch) if n.is_layer() || holders.contains(&n.id) => { - push(ch.get(usize::from(n.extras().shaper.is_some())..).unwrap_or_default(), moving, holders, out) - } - _ => out.extend(n.geometric_bounds()), - } - } - } - let holders: HashSet = moving.iter().filter_map(|id| doc.ancestry(*id)).flatten().collect(); - let mut out = vec![]; - push(&doc.layers, &moving.iter().copied().collect(), &holders, &mut out); - out -} - -impl Targets { - pub fn collect(doc: &Document, exclude: &[NodeId], visible: Option) -> Self { - Self::collect_inner(doc, exclude, None, visible, Gather::Bounds) - } - - /// Targets for moving the selection of `cx`: [`Self::collect`]'s but `exclude` (the selection, - /// or nothing for a copy, which leaves it behind), styled as `cx` asks, and with Smart Guides › - /// Spacing Guides the objects it spaces itself evenly with ([`Self::snap_rect`]). - pub fn for_move(cx: &ToolContext, exclude: &[NodeId]) -> Self { - let mut t = Self::collect(cx.doc, exclude, None).styled(cx); - if cx.spacing_guides { - t.boxes = object_boxes(cx.doc, &cx.selection.objects); - } - t - } - - /// Targets for scaling the selection: the other art, styled as `cx` asks, and with Smart Guides › - /// Spacing Guides the neighbours whose gaps the moving edge should match ([`Self::snap_scale`]). - pub fn for_scale(cx: &ToolContext) -> Self { - Self::for_move(cx, &cx.selection.objects) - } - - /// Targets for a drawn point: the art in `visible` (all of it for None) but `exclude`, its - /// anchors, centres, edges and path segments, its anchors in line too. The anchors and - /// segments of `exclude` (the path being drawn, whose bounds grow with it) stay targets. - pub fn for_drawing(doc: &Document, exclude: &[NodeId], visible: Option, glyphs: bool) -> Self { - let mut t = Self::collect_inner(doc, exclude, None, visible, Gather::Drawing); - t.push_paths(doc, exclude, true); - if glyphs { - t.push_glyphs(doc, exclude, visible); - } - t.index() - } - - /// Targets for a dragged direction handle of path `id`: the other art, and the anchors of `id` - /// (its bounds and the segments through the handle move with it, so they would chase it). - pub fn for_handle(doc: &Document, id: NodeId) -> Self { - let mut t = Self::collect_inner(doc, &[id], None, None, Gather::Bounds); - t.push_paths(doc, &[id], false); - t.index() - } - - /// The anchors of the paths `ids` as targets, on them and in line, and their segments too - /// when `segments`: at most [`BUDGET`] of each. - fn push_paths(&mut self, doc: &Document, ids: &[NodeId], segments: bool) { - let (mut anchors, mut segment_budget) = (BUDGET, if segments { BUDGET } else { 0 }); - for path in ids.iter().filter_map(|id| doc.node(*id)?.path_data()) { - for sp in &path.subpaths { - for a in sp.anchors.iter().take(anchors) { - self.push_anchor(a.p); - anchors -= 1; - } - self.push_segments(sp, |_| false, &mut segment_budget); - } - } - } - - /// Sort the targets so that [`within`] finds those near a point by bisection: the anchors - /// and centres by x, the lines by where they run. Every constructor ends with it (the sort - /// is stable: of targets alike, the first gathered still wins). - fn index(mut self) -> Self { - self.points.sort_by(|a, b| a.0.x.total_cmp(&b.0.x)); - self.xs.sort_by(|a, b| a.0.total_cmp(&b.0)); - self.ys.sort_by(|a, b| a.0.total_cmp(&b.0)); - self - } - - /// Anchor `a` as a target: a point lands on it and lines up with it. - fn push_anchor(&mut self, a: Point) { - self.points.push((a, Kind::Anchor)); - self.xs.push((a.x, a, Kind::Anchor)); - self.ys.push((a.y, a, Kind::Anchor)); - } - - /// Live-text baselines and glyph bounds (View › Snap to Glyph). - fn push_glyphs(&mut self, doc: &Document, exclude: &[NodeId], visible: Option) { - let skip: HashSet = exclude.iter().copied().collect(); - let db = FontDb::global(); - let kind = Kind::Glyph; - let mut budget = BUDGET / 4; - walk_but(&doc.layers, &skip, &mut |n| { - if budget == 0 || !n.visible { - return; - } - let NodeKind::Text(t) = &n.kind else { return }; - let lay = layout_text(db, t); - let ink = t.xf.transform_rect_bbox(lay.bounds); - if !ink.is_finite() || ink.width() <= 0.0 || ink.height() <= 0.0 { - return; - } - if let Some(v) = visible - && ink.intersect(v).area() <= 0.0 - && !v.contains(ink.center()) - { - return; - } - let to_doc = |p: Point| t.xf * p; - for line in &lay.lines { - let at = to_doc(Point::new(line.x0, line.baseline)); - self.ys.push((at.y, at, kind)); - } - for g in &lay.glyphs { - if budget == 0 { - break; - } - let bb = g.outline.bounding_box(); - if !(bb.width() > 0.0 && bb.height() > 0.0) { - continue; - } - let r = t.xf.transform_rect_bbox(bb); - for p in [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)] { - self.xs.push((p.x, p, kind)); - self.ys.push((p.y, p, kind)); - } - budget -= 1; - } - }); - } - - /// The same targets drawing their guides as the Smart Guides preferences in `cx` ask - /// ([`GuideStyle::of`]). - pub fn styled(mut self, cx: &ToolContext) -> Self { - self.style = GuideStyle::of(cx); - self - } - - /// An alignment line from `a` to `b`, unless Alignment Guides are off. - fn line(&self, a: Point, b: Point) -> Option { - self.style.lines.then_some(Overlay::Line { a, b, color: self.style.color, dashed: false }) - } - - /// A label `text` at `p`, unless Anchor/Path Labels are off. - fn label(&self, p: Point, text: &str) -> Option { - self.style.labels.then(|| Overlay::Label { p, text: text.into(), color: self.style.color }) - } - - /// Targets for dragging artboard `index`: everything except that artboard and `exclude` (the - /// art that moves along with it). - pub fn for_artboard(doc: &Document, index: usize, exclude: &[NodeId]) -> Self { - Self::collect_inner(doc, exclude, Some(index), None, Gather::Bounds) - } - - /// Targets for dragging the direct-selected anchors of `sel` (and its objects selected as a - /// whole): the other art, its path segments too, and the anchors and segments that stay put on - /// the paths being reshaped (their bounds move along, so they are no targets). A single anchor - /// also keeps to the extensions of the straight segments it ends (#2120). - pub fn for_anchor_drag(doc: &Document, sel: &Selection) -> Self { - let mut t = Self::collect_inner(doc, &sel.objects, None, None, Gather::Segments); - let (mut anchors, mut segments) = (BUDGET, BUDGET); - // With several anchors moving, the one under the pointer is not known here, and the - // others' extensions don't run through it. - let single = sel.objects.len() <= 1 && sel.anchors.values().map(|s| s.len()).sum::() == 1; - for (id, set) in &sel.anchors { - let Some(path) = doc.node(*id).and_then(|n| n.path_data()) else { continue }; - for (si, sp) in path.subpaths.iter().enumerate() { - let moving = |ai: usize| set.contains(&(si, ai)); - for a in sp.anchors.iter().enumerate().filter(|(ai, _)| !moving(*ai)).map(|(_, a)| a.p).take(anchors) { - t.push_anchor(a); - anchors -= 1; - } - t.push_segments(sp, moving, &mut segments); - if single { - t.push_extensions(sp, moving); - } - } - } - t.index() - } - - /// The extensions of the straight segments of `sp` with one end among the anchors `moving` - /// accepts and the other staying put. Horizontal and vertical ones are left out: lining up - /// with the other end already keeps the point on them, and says "align". - fn push_extensions(&mut self, sp: &SubPath, moving: impl Fn(usize) -> bool) { - let n = sp.anchors.len(); - for seg in (0..sp.segment_count()).filter(|&seg| moving(seg) != moving((seg + 1) % n) && sp.segment_is_line(seg)) { - let c = sp.segment(seg); - let (fixed, dragged) = if moving(seg) { (c.p3, c.p0) } else { (c.p0, c.p3) }; - let d = dragged - fixed; - let len = d.hypot(); - if len.is_finite() && d.x.abs() > 1e-9 * len && d.y.abs() > 1e-9 * len { - self.extensions.push((fixed, d / len)); - } - } - } - - /// The segments of `sp` that stay put while the anchors `moving` accepts move, as targets, at - /// most `budget` of them (counted down). - fn push_segments(&mut self, sp: &SubPath, moving: impl Fn(usize) -> bool, budget: &mut usize) { - let n = sp.anchors.len(); - for seg in 0..sp.segment_count() { - if *budget == 0 { - return; - } - if moving(seg) || moving((seg + 1) % n) { - continue; - } - let c = sp.segment(seg); - let bounds = Rect::from_points(c.p0, c.p1).union_pt(c.p2).union_pt(c.p3); - // A straight one as a line, so the point lands on it exactly. - self.segments.push((bounds, if sp.segment_is_line(seg) { PathSeg::Line(Line::new(c.p0, c.p3)) } else { PathSeg::Cubic(c) })); - *budget -= 1; - } - } - - fn collect_inner(doc: &Document, exclude: &[NodeId], skip_artboard: Option, visible: Option, gather: Gather) -> Self { - let mut t = Targets::default(); - let add_rect = |t: &mut Targets, r: Rect, kind: Kind| { - let c = r.center(); - for p in [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)] { - t.xs.push((p.x, p, kind)); - t.ys.push((p.y, p, kind)); - } - // A bleed shares its artboard's centre. - if kind != Kind::Bleed { - t.points.push((c, Kind::Center)); - t.xs.push((c.x, c, Kind::Center)); - t.ys.push((c.y, c, Kind::Center)); - } - }; - for (_, ab) in doc.artboards.iter().enumerate().filter(|(i, _)| Some(*i) != skip_artboard) { - add_rect(&mut t, ab.rect, Kind::Artboard); - if doc.setup.has_bleed() { - add_rect(&mut t, doc.setup.bleed_rect(ab.rect), Kind::Bleed); - } - } - let (mut budget, mut segment_budget) = (BUDGET, BUDGET); - walk_but(&doc.layers, &exclude.iter().copied().collect(), &mut |n| { - if budget == 0 || n.is_container() || !n.visible { - return; - } - let Some(b) = n.geometric_bounds() else { return }; - if let Some(v) = visible - && b.intersect(v).area() <= 0.0 - && !v.contains(b.center()) - { - return; - } - if let NodeKind::Path { path, .. } = &n.kind { - for (_, _, a) in path.anchors() { - if gather == Gather::Drawing { - t.push_anchor(a.p); - } else { - t.points.push((a.p, Kind::Anchor)); - } - budget = budget.saturating_sub(1); - } - if gather != Gather::Bounds { - for sp in &path.subpaths { - t.push_segments(sp, |_| false, &mut segment_budget); - } - } - } - add_rect(&mut t, b, Kind::Edge); - }); - t.index() - } - - /// Only these targets' anchors pull (View → Snap to Point). - pub(crate) fn anchors_only(mut self) -> Self { - self.points.retain(|(_, k)| *k == Kind::Anchor); - self.xs.clear(); - self.ys.clear(); - self.segments.clear(); - self.extensions.clear(); - self - } - - /// View → Snap to Point: only these targets' anchors pull, and the ruler guides (shown and - /// unlocked, as the selection tools pick them) pull the pointer into line where they run. - fn for_snap_to_point(self, cx: &ToolContext) -> Self { - self.anchors_only().with_rulers(cx) - } - - /// These targets and the ruler guides, when they are shown (and so unlocked to the selection - /// tools): they pull a point into line where they run. - fn with_rulers(mut self, cx: &ToolContext) -> Self { - if cx.guides { - self.rulers = cx - .doc - .guides - .iter() - .filter(|g| cx.doc.guide_shown(g)) - .map(|g| Ruler { vertical: g.vertical, pos: g.pos, span: cx.doc.guide_span(g) }) - .collect(); - } - self - } - - /// The ruler guides alone ([`Self::with_rulers`]): what a dragged selection's grabbed point - /// still snaps to with View → Snap to Point when Smart Guides line the selection up. - pub(crate) fn rulers(cx: &ToolContext) -> Self { - Self::default().styled(cx).with_rulers(cx) - } - - /// What a dragged selection's grabbed point snaps to with View → Snap to Point and Smart - /// Guides off: the anchors of the art but `exclude` and the ruler guides. - pub fn snap_to_point(cx: &ToolContext, exclude: &[NodeId]) -> Option { - (cx.snap_to_point && !cx.smart_guides).then(|| Self::collect(cx.doc, exclude, None).for_snap_to_point(cx)) - } - - /// The gridlines (one every `step` from the page's origin, as - /// [`vectorcraft_geom::snap::snap_to_grid`] places them) at and either side of the nearest one - /// to each of `points`, as lines to line up with. - fn grid_lines(points: impl IntoIterator, step: f64) -> Self { - let mut t = Targets::default(); - let line = |v: f64, k: i32| vectorcraft_geom::snap::snap_to_grid(v, step, 0.0) + f64::from(k) * step; - for p in points { - for k in -1..=1 { - let (x, y) = (line(p.x, k), line(p.y, k)); - t.xs.push((x, Point::new(x, p.y), Kind::Guide)); - t.ys.push((y, Point::new(p.x, y), Kind::Guide)); - } - } - t.index() - } - - /// Anchors and centres of the visible leaves under `roots` (the roots included) whose bounds - /// reach into `near`. - fn points_near(doc: &Document, roots: &[NodeId], near: Rect) -> Self { - let mut t = Targets::default(); - for n in roots.iter().filter_map(|id| doc.node(*id)) { - crate::draw2::walk_shown(n, &mut |c| { - if c.is_container() || !c.visible { - return; - } - let Some(b) = c.geometric_bounds() else { return }; - if b.x0 > near.x1 || b.x1 < near.x0 || b.y0 > near.y1 || b.y1 < near.y0 { - return; - } - if let NodeKind::Path { path, .. } = &c.kind { - t.points.extend(path.anchors().map(|(_, _, a)| (a.p, Kind::Anchor))); - } - t.points.push((b.center(), Kind::Center)); - }); - } - t.index() - } - - /// Snap a single point: onto the nearest anchor or centre, else onto the nearest segment, else - /// into line with targets. Returns the snapped point and guide overlays. - pub fn snap_point(&self, p: Point, tol: f64) -> (Point, Vec) { - self.snap_from(p, None, tol) - } - - /// Snap a drawn point `p` as [`Self::snap_point`] does, and when it leaves `from` (the anchor - /// before it, a shape's start): with Shift held it keeps to its angles round it, sliding along - /// that way into line with the nearest target; else, before lining up, it lands on a - /// construction guide through it, where a target's line crosses the guide if one does nearby. - pub fn snap_from(&self, p: Point, from: Option<&Leave>, tol: f64) -> (Point, Vec) { - if let Some(f) = from - && let Some(q) = f.constrained(p) - { - return match self.slide(f.at, q - f.at, q, tol, true) { - Some((q, target)) => (q, self.lined_up(q, target)), - None => (q, vec![]), - }; - } - if let Some((q, k)) = self.nearest_point(p, tol) { - return (q, self.label(q, k.label()).into_iter().collect()); - } - if let Some(q) = self.crossing(p, tol) { - return (q, self.label(q, "intersect").into_iter().collect()); - } - if let Some(f) = from.filter(|f| f.contact) - && let Some((q, perpendicular, dir)) = self.touching(f.at, p, tol) - { - return (q, self.touch_guides(f.at, q, perpendicular, dir, tol)); - } - if let Some(q) = self.on_segment(p, tol) { - return (q, self.label(q, Kind::Edge.label()).into_iter().collect()); - } - // A construction guide through the point it leaves, else a dragged segment's extension: - // it lands on the guide, sliding into line with a target crossing it nearby. - if let Some((at, dir, q)) = from.and_then(|f| f.construction(p, tol).map(|(dir, q)| (f.at, dir, q))).or_else(|| self.extension(p, tol)) { - let construction = |q: Point| Overlay::Line { a: at, b: q, color: self.style.color, dashed: false }; - return match self.slide(at, dir, q, tol, false) { - Some((q, target)) => (q, std::iter::once(construction(q)).chain(self.lined_up(q, target)).collect()), - None => (q, vec![construction(q)]), - }; - } - let mut out = p; - let mut ov = vec![]; - // The targets' lines along one axis and the ruler guides running past `p`, the nearest - // within reach of `v`. - let nearest = |lines: &[(f64, Point, Kind)], vertical: bool, v: f64| { - let rulers = self.rulers.iter().filter(|r| r.vertical == vertical).filter_map(|r| r.line_at(p, tol)); - lines_near(lines, v, tol) - .iter() - .copied() - .chain(rulers) - .filter(|(t, ..)| (t - v).abs() <= tol) - .min_by(|a, b| (a.0 - v).abs().total_cmp(&(b.0 - v).abs())) - }; - let mut aligned = false; - if let Some((x, from, _)) = nearest(&self.xs, true, p.x) { - out.x = x; - aligned = true; - ov.extend(self.line(from, Point::new(x, p.y))); - } - if let Some((y, from, _)) = nearest(&self.ys, false, p.y) { - out.y = y; - aligned = true; - ov.extend(self.line(from, Point::new(p.x, y))); - } - if aligned { - ov.extend(self.label(out, "align")); - } - (out, ov) - } - - /// Where a line from `at` through a point near `p` touches a target segment: square to it - /// (`perpendicular`, the foot of the perpendicular from `at`) or just grazing a curve - /// (`tangent`), within `tol` of `p`. The point, whether it is the perpendicular one, and the - /// segment's direction there. Targets gather segments for drawn points and dragged anchors only. - fn touching(&self, at: Point, p: Point, tol: f64) -> Option<(Point, bool, Vec2)> { - if !(self.tangent || self.perpendicular) || ![at.x, at.y, p.x, p.y, tol].iter().all(|v| v.is_finite()) { - return None; - } - let mut best: Option<(f64, Point, bool, Vec2)> = None; - // Every segment in reach is tried: their order is the document's, not the distance. - for (_, seg) in self.segments.iter().filter(|(b, _)| b.inflate(tol, tol).contains(p)) { - let curved = !matches!(seg, PathSeg::Line(_)); - for (perpendicular, wanted) in [(true, self.perpendicular && !curved), (false, self.tangent && curved)] { - if !wanted { - continue; - } - for t in touch_params(seg, at, perpendicular) { - let (q, d) = (seg.eval(t), seg_direction(seg, t)); - let dist = q.distance(p); - if dist <= tol && q.distance(at) > tol && best.is_none_or(|b| dist < b.0) { - best = Some((dist, q, perpendicular, d)); - } - } - } - } - best.map(|(_, q, perpendicular, d)| (q, perpendicular, d)) - } - - /// The guides of a point landed where the line from `at` touches a segment at `q` running - /// along `dir`: the line itself, a square corner mark for the perpendicular, and the label. - fn touch_guides(&self, at: Point, q: Point, perpendicular: bool, dir: Vec2, tol: f64) -> Vec { - let mut out: Vec = self.line(at, q).into_iter().collect(); - let (u, v) = (at - q, dir); - if perpendicular && self.style.lines && u.hypot2() > 1e-18 && v.hypot2() > 1e-18 { - let (u, v) = (u / u.hypot(), v / v.hypot()); - let s = tol * 1.5; - let mut mark = BezPath::new(); - mark.move_to(q + u * s); - mark.line_to(q + u * s + v * s); - mark.line_to(q + v * s); - out.push(Overlay::Path { path: mark, color: self.style.color, width: 1.0, dashed: false }); - } - out.extend(self.label(q, if perpendicular { "perpendicular" } else { "tangent" })); - out - } - - /// The segment extension nearest `p`, within `tol` of it (and `p` farther than that from the - /// end it starts at): that end, its direction and where `p` lands on it. Where `p` is nearer - /// the horizontal or the vertical through that end there is none: lining up with it ("align") - /// is what straightens a segment that is nearly level or upright. - fn extension(&self, p: Point, tol: f64) -> Option<(Point, Vec2, Point)> { - self.extensions - .iter() - .filter(|(at, _)| at.distance(p) > tol) - .map(|&(at, d)| (at, d, at + d * (p - at).dot(d))) - .map(|(at, d, q)| (at, d, q, q.distance(p))) - .filter(|(at, .., dist)| *dist <= tol && (p.x - at.x).abs().min((p.y - at.y).abs()) > *dist) - .min_by(|a, b| a.3.total_cmp(&b.3)) - .map(|(at, d, q, _)| (at, d, q)) - } - - /// The anchor or centre nearest `p`, within `tol`. - /// Is a target point (an anchor, a centre) within `tol` of `p`? - pub(crate) fn has_point_near(&self, p: Point, tol: f64) -> bool { - self.nearest_point(p, tol).is_some() - } - - fn nearest_point(&self, p: Point, tol: f64) -> Option<(Point, Kind)> { - within(&self.points, |q| q.0.x, p.x, tol) - .iter() - .copied() - .filter(|(q, _)| q.distance(p) <= tol) - .min_by(|a, b| a.0.distance(p).total_cmp(&b.0.distance(p))) - } - - /// Slide `q`, on the line through `at` along `dir` (beyond `at` only, for a `ray`), to where - /// the nearest target line crosses it, at most twice `tol` away: where it lands and the - /// target's own point (where its line starts). - fn slide(&self, at: Point, dir: Vec2, q: Point, tol: f64, ray: bool) -> Option<(Point, Point)> { - if !dir.hypot2().is_finite() || dir.hypot2() <= 1e-18 { - return None; - } - let reach = 2.0 * tol; - let mut best: Option<(f64, Point, Point)> = None; - for (vertical, lines, d, a, v) in [(true, &self.xs, dir.x, at.x, q.x), (false, &self.ys, dir.y, at.y, q.y)] { - if d.abs() <= 1e-9 { - continue; - } - // The ruler guides that way count as lines too (#812). - let rulers: Vec<(f64, Point, Kind)> = self.rulers.iter().filter(|r| r.vertical == vertical).filter_map(|r| r.line_at(q, reach)).collect(); - // A line that far across the way crosses it at least that far along. - for (t, from, _) in lines_near(lines, v, reach).iter().chain(&rulers) { - let s = (t - a) / d; - // Exactly on the line, whatever the rounding along the way. - let hit = if vertical { Point::new(*t, at.y + dir.y * s) } else { Point::new(at.x + dir.x * s, *t) }; - let moved = hit.distance(q); - if moved <= reach && (!ray || s > 0.0) && best.is_none_or(|b| moved < b.0) { - best = Some((moved, hit, *from)); - } - } - } - best.map(|(_, hit, from)| (hit, from)) - } - - /// The guides of a point `q` slid into line with the target at `from`: the line from it and - /// "align", or the target's own label when `q` sits right on it. - fn lined_up(&self, q: Point, from: Point) -> Vec { - if let Some((on, k)) = self.nearest_point(q, 1e-9) { - return self.label(on, k.label()).into_iter().collect(); - } - self.line(from, q).into_iter().chain(self.label(q, "align")).collect() - } - - /// The nearest point within `tol` of `p` on a target segment. - fn on_segment(&self, p: Point, tol: f64) -> Option { - self.segments - .iter() - .filter(|(b, _)| b.inflate(tol, tol).contains(p)) - .map(|(_, c)| (c, c.nearest(p, 1e-6))) - .filter(|(_, n)| n.distance_sq <= tol * tol) - .min_by(|a, b| a.1.distance_sq.total_cmp(&b.1.distance_sq)) - .map(|(c, n)| c.eval(n.t)) - } - - /// Where two target paths, a path and a ruler guide, or two ruler guides cross, the crossing - /// nearest `p` within `tol` ("intersect", #1602). - fn crossing(&self, p: Point, tol: f64) -> Option { - // A ruler guide running past, as a short line through the reach of `p`. - let ruler = |r: &Ruler| { - let (pos, ..) = r.line_at(p, tol)?; - let (across, along) = if r.vertical { (p.x, p.y) } else { (p.y, p.x) }; - let (a, b) = (along - tol, along + tol); - ((pos - across).abs() <= tol) - .then(|| PathSeg::Line(if r.vertical { Line::new((pos, a), (pos, b)) } else { Line::new((a, pos), (b, pos)) })) - }; - let near: Vec = self - .segments - .iter() - .filter(|(b, c)| b.inflate(tol, tol).contains(p) && c.nearest(p, 1e-6).distance_sq <= tol * tol) - .map(|(_, c)| *c) - .chain(self.rulers.iter().filter_map(ruler)) - // shortcut: the first 32 lines in reach are paired, raise it if busier crossings matter - .take(32) - .collect(); - let mut best: Option = None; - for (i, a) in near.iter().enumerate() { - for b in near.iter().skip(i + 1) { - let hits: Vec = match (a, b) { - (PathSeg::Line(l), c) | (c, PathSeg::Line(l)) => c.intersect_line(*l).iter().map(|x| l.eval(x.line_t)).collect(), - _ => meeting(a, b, p).into_iter().collect(), - }; - for q in hits { - if q.distance(p) <= tol && best.is_none_or(|b| q.distance(p) < b.distance(p)) { - best = Some(q); - } - } - } - } - best - } - - /// Snap a ruler guide at `v` (the x of a vertical one, the y of a horizontal one) into line - /// with the nearest edge, centre or anchor within `tol`: where it goes, and a label there. - pub fn snap_guide(&self, vertical: bool, v: f64, tol: f64) -> (f64, Vec) { - let along = |q: &Point| if vertical { q.x } else { q.y }; - let lines = if vertical { &self.xs } else { &self.ys }; - let best = lines_near(lines, v, tol) - .iter() - .map(|(t, from, k)| (*t, *from, *k)) - .chain(self.points.iter().map(|(q, k)| (along(q), *q, *k))) - .filter(|(t, ..)| (t - v).abs() <= tol) - .min_by(|a, b| (a.0 - v).abs().total_cmp(&(b.0 - v).abs())); - match best { - Some((t, from, k)) => (t, self.label(from, k.label()).into_iter().collect()), - None => (v, vec![]), - } - } - - /// Snap a dragged point that carries others along at `offsets` (a resize handle and the - /// bleed edge beyond it): onto an anchor or centre near the point itself, otherwise into line - /// with targets, each axis on whichever of them comes nearest. - pub fn snap_point_with(&self, p: Point, offsets: &[Vec2], tol: f64) -> (Point, Vec) { - if self.nearest_point(p, tol).is_some() { - return self.snap_point(p, tol); - } - let rects: Vec = std::iter::once(Vec2::ZERO).chain(offsets.iter().copied()).map(|o| Rect::from_points(p + o, p + o)).collect(); - let (adj, mut ov, aligned) = self.align_rects(&rects, tol); - let out = p + adj; - if aligned.contains(&true) { - ov.extend(self.label(out, "align")); - } - (out, ov) - } - - /// Snap a moving rectangle (selection bounds after a move by `d`): tries its corners/edges/centre. - pub fn snap_rect(&self, r: Rect, tol: f64) -> (Vec2, Vec) { - self.snap_rects(&[r], tol) - } - - /// Snap a point that may only move along `dir` (the grabbed point of a move Shift keeps to its - /// line): onto the point of that line nearest an anchor or center within `tol` of `p`, labeled - /// at the anchor or center, else to where the nearest target line or ruler guide crosses the - /// line, at most twice `tol` away ([`Self::slide`]). Returns where it goes and the guides. - pub(crate) fn snap_point_along(&self, p: Point, dir: Vec2, tol: f64) -> (Point, Vec) { - let l2 = dir.hypot2(); - if !l2.is_finite() || l2 <= 1e-18 { - return (p, vec![]); - } - if let Some((q, k)) = self.nearest_point(p, tol) { - return (p + dir * ((q - p).dot(dir) / l2), self.label(q, k.label()).into_iter().collect()); - } - match self.slide(p, dir, p, tol, false) { - Some((q, from)) => (q, self.lined_up(q, from)), - None => (p, vec![]), - } - } - - /// [`Self::snap_rects`] for rectangles that may only move along `dir` (a move Shift keeps to - /// its line): they slide along it until the edge or center of one of them is in line with a - /// target, within `tol` of it on that axis, the target that needs the shortest way along the - /// line winning. Lining up with nothing, a single rectangle with Spacing Guides slides to where - /// it is spaced evenly along an axis the line runs along. Returns the shift (along `dir`) and - /// the guides. - pub(crate) fn snap_rects_along(&self, rects: &[Rect], dir: Vec2, tol: f64) -> (Vec2, Vec) { - let len = dir.hypot(); - if !len.is_finite() || len <= 1e-9 { - return (Vec2::ZERO, vec![]); - } - let u = dir / len; - // The axes the line runs along: the x axis (`true`) and the y axis, and how far a step of - // one along the line goes on that axis. - let axes = [(true, u.x), (false, u.y)].into_iter().filter(|(_, c)| c.abs() > 1e-9); - // The way along the line, the target line it lines up with, where that line starts, and - // its axis. - let mut best: Option<(f64, f64, Point, bool)> = None; - for (x, c) in axes.clone() { - let lines = if x { &self.xs } else { &self.ys }; - for r in rects { - let vs = if x { [r.x0, r.center().x, r.x1] } else { [r.y0, r.center().y, r.y1] }; - for v in vs { - for (t, from, _) in lines_near(lines, v, tol).iter().filter(|(t, ..)| (t - v).abs() <= tol) { - let s = (t - v) / c; - if best.is_none_or(|b| s.abs() < b.0.abs()) { - best = Some((s, *t, *from, x)); - } - } - } - } - } - if let Some((s, t, from, x)) = best { - let ov = rects.iter().map(|r| *r + u * s).reduce(|a, b| a.union(b)).and_then(|m| { - if x { - self.line(Point::new(t, from.y.min(m.y0)), Point::new(t, from.y.max(m.y1))) - } else { - self.line(Point::new(from.x.min(m.x0), t), Point::new(from.x.max(m.x1), t)) - } - }); - return (u * s, ov.into_iter().collect()); - } - let [r] = rects else { return (Vec2::ZERO, vec![]) }; - if self.boxes.is_empty() { - return (Vec2::ZERO, vec![]); - } - let spaced = - axes.filter_map(|(x, c)| self.even_spacing(*r, x, tol).map(|(shift, _)| (shift / c, x))).min_by(|a, b| a.0.abs().total_cmp(&b.0.abs())); - let Some((s, x)) = spaced else { return (Vec2::ZERO, vec![]) }; - let gaps = self.even_spacing(*r + u * s, x, tol).map(|(_, gaps)| gaps).unwrap_or_default(); - (u * s, gaps.into_iter().flat_map(|g| self.gap_marks(g, x, tol)).collect()) - } - - /// Snap rectangles that move together (an artboard and its bleed): the edge or centre of any - /// of them nearest a target, per axis. A single moved selection with Spacing Guides - /// ([`Self::for_move`]) is spaced evenly along an axis that lines up with nothing - /// ([`Self::even_spacing`]). Returns the shift and the guides. - pub fn snap_rects(&self, rects: &[Rect], tol: f64) -> (Vec2, Vec) { - let (mut d, mut ov, aligned) = self.align_rects(rects, tol); - if let [r] = rects - && !self.boxes.is_empty() - { - let moved = *r + d; - let mut spaced = false; - for (x, lined_up) in [true, false].into_iter().zip(aligned) { - if let Some((shift, _)) = self.even_spacing(moved, x, tol).filter(|_| !lined_up) { - *(if x { &mut d.x } else { &mut d.y }) += shift; - spaced = true; - } - } - // The equal gaps, marked where the rectangle ends up. - if spaced { - let done = *r + d; - for (x, lined_up) in [true, false].into_iter().zip(aligned) { - let gaps = if lined_up { None } else { self.even_spacing(done, x, tol) }; - ov.extend(gaps.into_iter().flat_map(|(_, gaps)| gaps).flat_map(|g| self.gap_marks(g, x, tol))); - } - } - } - (d, ov) - } - - /// Spacing Guides along the x axis (`x`) or the y axis for the moved rectangle `r`, among the - /// objects of its row (column): the shift (within `tol`) that leaves it as far from its - /// neighbour before or after it as two other objects of the row are apart, or as far from both - /// neighbours; and the equal gaps then, as (start, end, where across) along that axis. - fn even_spacing(&self, r: Rect, x: bool, tol: f64) -> Option<(f64, Vec<[f64; 3]>)> { - let (r0, r1) = span(&r, x); - let len = r1 - r0; - let (c0, c1) = span(&r, !x); - // The objects beside it, before or after, overlapping it across, by where they start. - let mut row: Vec<(Rect, f64, f64)> = self - .boxes - .iter() - .map(|b| (*b, span(b, x).0, span(b, x).1)) - .filter(|(b, a0, a1)| span(b, !x).0 < c1 && span(b, !x).1 > c0 && (*a1 <= r0 + tol || *a0 >= r1 - tol)) - .collect(); - row.sort_by(|a, b| a.1.total_cmp(&b.1)); - // Where two boxes overlap across: the middle of that (or of the space between them). - let mid = |a: &Rect, b: &Rect| (span(a, !x).0.max(span(b, !x).0) + span(a, !x).1.min(span(b, !x).1)) / 2.0; - // Each object and the nearest one after it, unless the moved rectangle sits between them. - let pairs: Vec<(f64, [f64; 3])> = row - .iter() - .filter_map(|(p, _, p1)| { - let next = row.partition_point(|(_, a0, _)| *a0 < *p1); - let (q, q0, _) = row.get(next)?; - let straddles = *p1 <= r0 + tol && *q0 >= r1 - tol; - (*q0 > *p1 && !straddles).then(|| (q0 - p1, [*p1, *q0, mid(p, q)])) - }) - .collect(); - let before = row.iter().filter(|(_, _, a1)| *a1 <= r0 + tol).max_by(|a, b| a.2.total_cmp(&b.2)); - let after = row.iter().filter(|(_, a0, _)| *a0 >= r1 - tol).min_by(|a, b| a.1.total_cmp(&b.1)); - // Where it could start, and the gap it repeats (None: halfway between its neighbours). - let mut starts: Vec<(f64, Option)> = vec![]; - if let (Some(a), Some(b)) = (before, after) - && b.1 - a.2 > len - { - starts.push(((a.2 + b.1 - len) / 2.0, None)); - } - for (g, _) in &pairs { - starts.extend(before.map(|a| (a.2 + g, Some(*g)))); - starts.extend(after.map(|b| (b.1 - g - len, Some(*g)))); - } - let (start, repeats) = - starts.into_iter().filter(|(t, _)| (t - r0).abs() <= tol).min_by(|a, b| (a.0 - r0).abs().total_cmp(&(b.0 - r0).abs()))?; - let at = r + if x { Vec2::new(start - r0, 0.0) } else { Vec2::new(0.0, start - r0) }; - let (end, mut gaps) = (start + len, vec![]); - if let Some((a, _, a1)) = before - && repeats.is_none_or(|g| (start - a1 - g).abs() <= 1e-9) - { - gaps.push([*a1, start, mid(a, &at)]); - } - if let Some((b, b0, _)) = after - && repeats.is_none_or(|g| (b0 - end - g).abs() <= 1e-9) - { - gaps.push([end, *b0, mid(b, &at)]); - } - if let Some(g) = repeats { - gaps.extend(pairs.iter().filter(|(pg, _)| (pg - g).abs() <= 1e-9).map(|(_, gap)| *gap)); - } - Some((start - r0, gaps)) - } - - /// A Spacing Guides mark: a line across the gap (start, end, where across) along the x axis - /// (`x`) or the y axis, with a tick `tick` long either side of it at each end. - fn gap_marks(&self, [u0, u1, c]: [f64; 3], x: bool, tick: f64) -> [Overlay; 3] { - let pt = |u: f64, c: f64| if x { Point::new(u, c) } else { Point::new(c, u) }; - let line = |a: Point, b: Point| Overlay::Line { a, b, color: self.style.color, dashed: false }; - [line(pt(u0, c), pt(u1, c)), line(pt(u0, c - tick), pt(u0, c + tick)), line(pt(u1, c - tick), pt(u1, c + tick))] - } - - /// [`Self::snap_rects`] before Spacing Guides, also saying whether each axis (x, y) lined up - /// (even with Alignment Guides off, or already in line, when there is no line or shift to - /// tell). - fn align_rects(&self, rects: &[Rect], tol: f64) -> (Vec2, Vec, [bool; 2]) { - let nearest = |targets: &[(f64, Point, Kind)], along: fn(&Rect) -> [f64; 3]| { - rects - .iter() - .flat_map(|r| along(r).into_iter().map(move |v| (v, *r))) - .flat_map(|(v, r)| lines_near(targets, v, tol).iter().map(move |(t, from, _)| (t - v, *from, v, r))) - .filter(|(d, ..)| d.abs() <= tol) - .min_by(|a, b| a.0.abs().total_cmp(&b.0.abs())) - }; - let best_x = nearest(&self.xs, |r| [r.x0, r.center().x, r.x1]); - let best_y = nearest(&self.ys, |r| [r.y0, r.center().y, r.y1]); - let mut d = Vec2::ZERO; - let mut ov = vec![]; - if let Some((dx, from, x, r)) = best_x { - d.x = dx; - let x = x + dx; - let (y0, y1) = (from.y.min(r.y0), from.y.max(r.y1)); - ov.extend(self.line(Point::new(x, y0), Point::new(x, y1))); - } - if let Some((dy, from, y, r)) = best_y { - d.y = dy; - let y = y + dy; - let (x0, x1) = (from.x.min(r.x0), from.x.max(r.x1)); - ov.extend(self.line(Point::new(x0, y), Point::new(x1, y))); - } - (d, ov, [best_x.is_some(), best_y.is_some()]) - } - - /// Snap a bounding-box resize. `a` is the scale [`crate::bbox::scale_for_drag`] gave for - /// dragging `handle` of the box `bx` (about the centre when `from_center`), in the box's own - /// frame, and so is the result. The handle lands on an anchor or centre within `tol` on the page - /// (labelled as [`Self::snap_point`] does), else the corners and the side it moves line up with - /// the nearest target or ruler guide ("align"), whatever the box's angle. A handle with one way - /// to go (a side, or a proportional corner) slides along it until one of them is on a target. - pub fn snap_scale(&self, bx: &OrientedBox, handle: Handle, a: Affine, proportional: bool, from_center: bool, tol: f64) -> (Affine, Vec) { - type Hit = Option<(f64, Point)>; - // Where a snap lands: in line with a target's x and/or y, or on an anchor or centre. - type Snap = (Hit, Hit, Option<(Point, Kind)>); - let r = bx.rect; - let origin = if from_center { r.center() } else { handle.opposite().pos(r) }; - let (d0, [sx, _, _, sy, _, _]) = (handle.pos(r) - origin, a.as_coeffs()); - let (to_doc, to_local) = (bx.to_doc(), bx.to_doc().inverse()); - // A scale that collapses or flips the box is no snap. - let valid = |n: f64, s: f64| n.is_finite() && n * s > 0.0; - // The target line along the x axis (`x`: a vertical line) or the y axis nearest `p`, the - // ruler guides running past it included. - let nearest = |p: Point, x: bool| -> Hit { - let (v, ts) = if x { (p.x, &self.xs) } else { (p.y, &self.ys) }; - let rulers = self.rulers.iter().filter(|r| r.vertical == x).filter_map(|r| r.line_at(p, tol)); - lines_near(ts, v, tol) - .iter() - .copied() - .chain(rulers) - .map(|(t, from, _)| (t, from)) - .filter(|(t, _)| (t - v).abs() <= tol) - .min_by(|p, q| (p.0 - v).abs().total_cmp(&(q.0 - v).abs())) - }; - let (mut nx, mut ny) = (sx, sy); - let mut snap: Snap = (None, None, None); - if handle.is_corner() && !proportional { - // The corner goes anywhere: onto an anchor or centre, else it takes the target's x, its - // y, or both. - let p = to_doc * (a * handle.pos(r)); - let (hx, hy) = (nearest(p, true), nearest(p, false)); - let on = self.nearest_point(p, tol).map(|(q, k)| (Some((q.x, q)), Some((q.y, q)), Some((q, k)))); - for (tx, ty, at) in on.into_iter().chain([(hx, hy, None), (hx, None, None), (None, hy, None)]) { - if tx.is_none() && ty.is_none() { - continue; - } - let q = to_local * Point::new(tx.map_or(p.x, |h| h.0), ty.map_or(p.y, |h| h.0)); - let (mut cx, mut cy) = ((q.x - origin.x) / d0.x, (q.y - origin.y) / d0.y); - // Square to the page, the axis without a target stays exactly where it was. - if bx.angle == 0.0 { - (cx, cy) = (if tx.is_some() { cx } else { sx }, if ty.is_some() { cy } else { sy }); - } - if valid(cx, sx) && valid(cy, sy) { - (nx, ny, snap) = (cx, cy, (tx, ty, at)); - break; - } - } - } else { - // One parameter `s`: the scale is `c + s·e` on each axis. - let (ax, _) = handle.axes(); - let (e, c, s0) = match (handle.is_corner(), ax, proportional) { - (true, _, _) => ((sx.signum(), sy.signum()), (0.0, 0.0), sx.abs()), - (false, true, true) => ((1.0, sx.signum()), (0.0, 0.0), sx), - (false, true, false) => ((1.0, 0.0), (0.0, 1.0), sx), - (false, false, true) => ((sy.signum(), 1.0), (0.0, 0.0), sy), - (false, false, false) => ((0.0, 1.0), (1.0, 0.0), sy), - }; - // The points that may land on a target: the handle, and the two ends of its side. - let i = handle as usize; - let ends = if handle.is_corner() { [None, None] } else { [Handle::ALL.get((i + 1) % 8), Handle::ALL.get((i + 7) % 8)] }; - // How far the handle travels per unit of `s`: a snap may not drag it far from the - // pointer, as it would for a target the side runs almost along. - let travel = (e.0 * d0.x).hypot(e.1 * d0.y); - // The snap so far: its `s`, whether it only lines up (an anchor or centre comes first), - // how far it moves the handle, and where it lands. - let mut best: Option<(f64, bool, f64, Snap)> = None; - let mut consider = |s: f64, in_line: bool, found: Snap| { - let moved = (s - s0).abs() * travel; - if valid(s, s0) && moved <= 2.0 * tol && best.as_ref().is_none_or(|k| (in_line, moved) < (k.1, k.2)) { - best = Some((s, in_line, moved, found)); - } - }; - for h in std::iter::once(handle).chain(ends.into_iter().flatten().copied()) { - let v = h.pos(r) - origin; - let base = to_doc * (origin + Vec2::new(c.0 * v.x, c.1 * v.y)); - let dir = (to_doc * Point::new(e.0 * v.x, e.1 * v.y)).to_vec2(); - let p = base + dir * s0; - // An anchor or centre beside its way: it stops at the nearest point of the way. - let l2 = dir.hypot2(); - if l2 > 1e-18 - && let Some((q, k)) = self.nearest_point(p, tol) - { - consider((q - base).dot(dir) / l2, false, (None, None, Some((q, k)))); - } - for (is_x, b, d) in [(true, base.x, dir.x), (false, base.y, dir.y)] { - if d.abs() <= 1e-9 { - continue; - } - let Some(hit) = nearest(p, is_x) else { continue }; - consider((hit.0 - b) / d, true, if is_x { (Some(hit), None, None) } else { (None, Some(hit), None) }); - } - } - if let Some((s, _, _, found)) = best { - (nx, ny, snap) = (c.0 + s * e.0, c.1 + s * e.1, found); - } - } - let mut out = Affine::translate(origin.to_vec2()) * Affine::scale_non_uniform(nx, ny) * Affine::translate(-origin.to_vec2()); - let (hit_x, hit_y, on) = snap; - if let Some((q, k)) = on { - return (out, self.label(q, k.label()).into_iter().collect()); - } - // The lines run from the target's own point across the resized box, on the page. - let mut nr = (to_doc * out).transform_rect_bbox(r); - let mut ov = vec![]; - if let Some((x, from)) = hit_x { - ov.extend(self.line(Point::new(x, from.y.min(nr.y0)), Point::new(x, from.y.max(nr.y1)))); - } - if let Some((y, from)) = hit_y { - ov.extend(self.line(Point::new(from.x.min(nr.x0), y), Point::new(from.x.max(nr.x1), y))); - } - if hit_x.is_some() || hit_y.is_some() { - ov.extend(self.label(to_doc * (out * handle.pos(r)), "align")); - } - if !self.boxes.is_empty() && !from_center { - let aligned = [hit_x.is_some(), hit_y.is_some()]; - for (is_x, lined_up) in [(true, aligned[0]), (false, aligned[1])] { - if lined_up { - continue; - } - let Some(moves_min) = resize_moves_min_edge(handle, is_x) else { continue }; - if let Some((shift, gaps)) = self.even_spacing(nr, is_x, tol) { - let (lo, hi) = span(&nr, is_x); - let len = hi - lo; - let new_len = if moves_min { hi - (lo + shift) } else { hi + shift - lo }; - if len > 1e-9 && new_len > 1e-9 { - let f = new_len / len; - let (ax, ay) = handle.axes(); - if is_x { - nx *= f; - if proportional && ay { - ny *= f; - } - } else { - ny *= f; - if proportional && ax { - nx *= f; - } - } - out = Affine::translate(origin.to_vec2()) * Affine::scale_non_uniform(nx, ny) * Affine::translate(-origin.to_vec2()); - nr = (to_doc * out).transform_rect_bbox(r); - ov.extend(gaps.into_iter().flat_map(|g| self.gap_marks(g, is_x, tol))); - } - } - } - } - (out, ov) - } -} - -/// Where curves `a` and `b` cross near `p`: a point projected from one onto the other and back -/// settles where they meet (none when they don't nearby). -fn meeting(a: &PathSeg, b: &PathSeg, p: Point) -> Option { - let mut q = p; - for _ in 0..32 { - let on_a = a.eval(a.nearest(q, 1e-9).t); - q = b.eval(b.nearest(on_a, 1e-9).t); - if on_a.distance(q) <= 1e-6 { - return Some(q); - } - } - None -} - -/// The direction of `seg` at `t` (not normalised; zero in a cusp). -fn seg_direction(seg: &PathSeg, t: f64) -> Vec2 { - match seg { - PathSeg::Line(l) => l.p1 - l.p0, - PathSeg::Quad(q) => q.deriv().eval(t).to_vec2(), - PathSeg::Cubic(c) => c.deriv().eval(t).to_vec2(), - } -} - -/// The parameters in (0, 1) where the line from `at` to `seg` is square to it (`perpendicular`) -/// or grazes it, found by sampling the curve for a change of sign and bisecting. -fn touch_params(seg: &PathSeg, at: Point, perpendicular: bool) -> Vec { - const STEPS: usize = 24; - let f = |t: f64| { - let (v, d) = (seg.eval(t) - at, seg_direction(seg, t)); - if perpendicular { v.dot(d) } else { v.cross(d) } - }; - let mut out = vec![]; - let mut prev = (0.0, f(0.0)); - for i in 1..=STEPS { - let t = i as f64 / STEPS as f64; - let cur = (t, f(t)); - if prev.1.is_finite() && cur.1.is_finite() && prev.1 * cur.1 < 0.0 { - let (mut lo, mut hi) = (prev, cur); - for _ in 0..40 { - let mid = 0.5 * (lo.0 + hi.0); - let m = (mid, f(mid)); - if lo.1 * m.1 <= 0.0 { - hi = m; - } else { - lo = m; - } - } - let t = 0.5 * (lo.0 + hi.0); - // A jump through a cusp changes sign without crossing zero: the line must really - // run along (or square to) the curve there. - let (v, d) = (seg.eval(t) - at, seg_direction(seg, t)); - let scale = v.hypot() * d.hypot(); - if scale > 1e-12 && f(t).abs() <= 1e-3 * scale { - out.push(t); - } - } - prev = cur; - } - out -} - -/// Where a press at `p` takes hold of `path` by its outline: the point of the path nearest `p`, -/// when it is within `reach`, and on a straight segment exactly on its line. A path dragged by a -/// press on its stroke or beside its edge snaps by that point of the path, so it is the path that -/// lands on a ruler guide or lines up, not the pressed spot a few pixels beside it (#3701). -pub(crate) fn hold_on_path(path: &vectorcraft_geom::PathData, p: Point, reach: f64) -> Option { - let (si, seg, _, q, d) = path.nearest(p)?; - if d > reach { - return None; - } - let sp = path.subpaths.get(si)?; - if !sp.segment_is_line(seg) { - return Some(q); - } - let c = sp.segment(seg); - let line = Line::new(c.p0, c.p3); - Some(line.eval(line.nearest(p, 0.0).t)) -} - -/// Snap `p` for a drawing tool when smart guides (or grid snapping) are on. -pub fn snap_draw(cx: &ToolContext, p: Point, exclude: &[NodeId]) -> (Point, Vec) { - snap_with(cx, p, || Targets::collect(cx.doc, exclude, None)) -} - -/// Snap `p` to pixels or the grid when they are on, else to the smart guide `targets`, else (Snap -/// to Point) to their anchors and the ruler guides within the Snap to Point distance. -fn snap_with(cx: &ToolContext, p: Point, targets: impl FnOnce() -> Targets) -> (Point, Vec) { - PointSnap::new(cx, targets).snap(cx, p) -} - -/// Snapping for a dragged point, its targets gathered once (when the drag begins): what -/// [`snap_draw`] does for a point at a time. -pub struct PointSnap { - /// The smart guide targets, or with Smart Guides off those of Snap to Point, and how near (in - /// screen pixels) they pull; none with pixels or the grid snapping instead, or nothing on. - targets: Option<(Targets, f64)>, -} - -impl PointSnap { - pub fn new(cx: &ToolContext, targets: impl FnOnce() -> Targets) -> Self { - let targets = if cx.snap_to_pixel || cx.snap_to_grid { - None - } else if cx.smart_guides { - // Snap to Point keeps pulling points onto the ruler guides with Smart Guides on (#812). - let mut t = targets().styled(cx); - (t.tangent, t.perpendicular) = (cx.snap_tangent, cx.snap_perpendicular); - Some((if cx.snap_to_point { t.with_rulers(cx) } else { t }, cx.snapping_tolerance)) - } else if cx.snap_to_point { - Some((targets().for_snap_to_point(cx), cx.snap_tolerance)) - } else { - None - }; - Self { targets } - } - - /// Where `p` goes, and the guides showing why. - pub fn snap(&self, cx: &ToolContext, p: Point) -> (Point, Vec) { - self.snap_from(cx, p, None) - } - - /// Where a drawn point `p` leaving `from` goes ([`Targets::snap_from`]), and the guides - /// showing why. Pixels and the grid round it before Shift turns it onto its angle. - pub fn snap_from(&self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> (Point, Vec) { - let keep = |q: Point| from.and_then(|f| f.constrained(q)).unwrap_or(q); - if cx.snap_to_pixel { - return (keep(Point::new(p.x.round(), p.y.round())), vec![]); - } - if cx.snap_to_grid { - return (keep(vectorcraft_geom::snap::snap_point_to_grid(p, cx.grid_step())), vec![]); - } - match &self.targets { - Some((t, tol)) => t.snap_from(p, from, cx.tol(*tol)), - None => (keep(p), vec![]), - } - } - - /// Snap a bounding-box resize ([`Targets::snap_scale`]) as [`Self::snap`] snaps a point. With - /// Snap to Pixel on, the corner or side the handle moves lines up with whole pixels, and with - /// Snap to Grid on it lines up with the nearest gridline, however far that is; neither shows - /// guides. Otherwise the handle snaps to the targets [`Self::new`] gathered. - pub fn snap_scale( - &self, - cx: &ToolContext, - bx: &OrientedBox, - handle: Handle, - a: Affine, - proportional: bool, - from_center: bool, - ) -> (Affine, Vec) { - let step = if cx.snap_to_pixel { - Some(1.0) - } else if cx.snap_to_grid { - Some(cx.grid_step()).filter(|s| s.is_finite() && *s > 0.0) - } else { - None - }; - if let Some(step) = step { - let moved = Handle::ALL.map(|h| bx.to_doc() * (a * h.pos(bx.rect))); - // Every point the handle moves is within half a step of a gridline, so a tolerance of - // one step always finds one. - return (Targets::grid_lines(moved, step).snap_scale(bx, handle, a, proportional, from_center, step).0, vec![]); - } - match &self.targets { - Some((t, tol)) => t.snap_scale(bx, handle, a, proportional, from_center, cx.tol(*tol)), - None => (a, vec![]), - } - } -} - -/// The Construction Guides angle set new preferences start with. -pub const DEFAULT_CONSTRUCTION_ANGLES: &str = "90° & 45° Angles"; - -/// The angles (degrees) of the construction guides of a Smart Guides › Angles set: "90°" gives the -/// horizontal and the vertical, "45°" the diagonals, "60°" and "30°" their multiples between those. -pub fn construction_angles(set: &str) -> &'static [f64] { - match set { - "30° Angles" => &[30.0, 60.0, 120.0, 150.0], - "45° Angles" => &[45.0, 135.0], - "60° Angles" => &[60.0, 120.0], - "90° Angles" => &[0.0, 90.0], - "90° & 45° & 30° Angles" => &[0.0, 30.0, 45.0, 60.0, 90.0, 120.0, 135.0, 150.0], - _ => &[0.0, 45.0, 90.0, 135.0], - } -} - -/// How Shift keeps a drawn point to the point it leaves from. -#[derive(Clone, Copy, Debug, PartialEq)] -enum Keep { - /// At steps of `step` degrees counted from `base` (a segment). - Angle { step: f64, base: f64 }, - /// On a diagonal, as far along as the pointer's farther axis (a square's corner). - Square, -} - -/// The point a drawn point leaves from (the anchor before it, a shape's start) and how it keeps -/// to it: with Shift held at fixed angles round it, and onto the construction guides through it -/// (Smart Guides › Construction Guides). -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct Leave { - pub at: Point, - /// Shift held: how the point keeps to `at`. - shift: Option, - /// The angles of the construction guides through `at` (none with them off). - angles: &'static [f64], - /// A drawn segment: it may land square to a segment or grazing a curve (a box does not). - contact: bool, -} - -impl Leave { - /// A segment from `at` (Pen, Curvature, Line Segment): Shift keeps it at 45° steps from - /// General › Constrain Angle; with Smart Guides on it lands on the construction guides. - pub fn segment(cx: &ToolContext, at: Point, shift: bool) -> Self { - Self { - at, - shift: shift.then_some(Keep::Angle { step: 45.0, base: cx.constrain_angle }), - angles: if cx.smart_guides { cx.construction_angles } else { &[] }, - contact: true, - } - } - - /// A curved source cannot make a line-to-line perpendicular or line-to-curve tangent contact. - pub fn without_contact(mut self) -> Self { - self.contact = false; - self - } - - /// A box dragged from its corner or centre `at` (Rectangle, Ellipse, Arc, the grids, and - /// Artboard, which passes no Shift): Shift keeps the dragged corner on a diagonal, the box a - /// square. - pub fn diagonal(at: Point, shift: bool) -> Self { - Self { at, shift: shift.then_some(Keep::Square), angles: &[], contact: false } - } - - /// Where Shift puts `p`: none without it. - fn constrained(&self, p: Point) -> Option { - let v = p - self.at; - Some(match self.shift? { - Keep::Angle { step, base } => self.at + constrain_angle_from(v, step, base), - Keep::Square => self.at + square(v), - }) - } - - /// The construction guide through `at` nearest `p`, within `tol` of it (and `p` farther than - /// that from `at`, where every guide meets): its direction and where `p` lands on it. - fn construction(&self, p: Point, tol: f64) -> Option<(Vec2, Point)> { - let v = p - self.at; - if v.hypot() <= tol { - return None; - } - self.angles - .iter() - // Counter-clockwise as seen on the y-down page. - .map(|a| Vec2::new(a.to_radians().cos(), -a.to_radians().sin())) - .map(|d| (d, self.at + d * v.dot(d))) - .map(|(d, q)| (d, q, q.distance(p))) - .filter(|(.., dist)| *dist <= tol) - .min_by(|a, b| a.2.total_cmp(&b.2)) - .map(|(d, q, _)| (d, q)) - } -} - -/// `d` made square: as long on both axes as on its longer one, each keeping its sign. -pub fn square(d: Vec2) -> Vec2 { - let s = d.x.abs().max(d.y.abs()); - Vec2::new(s * d.x.signum(), s * d.y.signum()) -} - -/// Smart Guides for the points a drawing tool places: the Pen's and Curvature tool's anchors, -/// the Pencil's ends, the shape tools' start and dragged corner, the Type tool's click and the -/// Artboard tool's new artboard. Each such tool keeps one and asks it for every point, hovering -/// too, so they all snap alike, and shows its [`Self::guides`]. -/// -/// The targets ([`Targets::for_drawing`], the art in the window) are gathered once per document -/// state: hovering gathers them again only after the document, the selection, what is left out, -/// the window or the snapping options changed; a press always gathers them afresh, and the drag -/// it starts keeps them, the document then showing the drag's own previews. -#[derive(Default)] -pub struct DrawSnap { - /// The targets, and what they were gathered for (none: for one drag only). - targets: Option<(Option, PointSnap)>, - guides: Vec, -} - -/// What a [`DrawSnap`]'s targets were gathered for: the document state, the art left out, the -/// window and every option [`PointSnap::new`] reads. -#[derive(PartialEq)] -struct SnapKey { - revision: (u64, u64), - exclude: Vec, - screen: Option, - view: [bool; 7], - snap_to_glyph: bool, - style: GuideStyle, - tolerances: [f64; 2], -} - -impl SnapKey { - fn of(cx: &ToolContext, exclude: &[NodeId]) -> Self { - Self { - revision: cx.revision, - exclude: exclude.to_vec(), - screen: cx.screen, - view: [cx.smart_guides, cx.snap_to_point, cx.snap_to_pixel, cx.snap_to_grid, cx.guides, cx.snap_tangent, cx.snap_perpendicular], - snap_to_glyph: cx.snap_to_glyph && cx.alignment_guides, - style: GuideStyle::of(cx), - tolerances: [cx.snapping_tolerance, cx.snap_tolerance], - } - } -} - -/// The part of the document the window shows (none headless: all of it). -fn visible(cx: &ToolContext) -> Option { - let s = cx.screen?; - let (w, h) = s.size; - Some(Rect::from_points(s.at(0.0, 0.0), s.at(w, h)).union_pt(s.at(w, 0.0)).union_pt(s.at(0.0, h))) -} - -impl DrawSnap { - /// Snap the pointer hovering at `p` (the art but `exclude` as targets, the anchors of - /// `exclude` still pulling), reusing the targets while nothing they depend on changed. - pub fn hover(&mut self, cx: &ToolContext, p: Point, exclude: &[NodeId], from: Option<&Leave>) -> Point { - let key = SnapKey::of(cx, exclude); - if self.targets.as_ref().is_none_or(|(k, _)| k.as_ref() != Some(&key)) { - self.gather(cx, key); - } - self.snap(cx, p, from) - } - - /// Snap the point pressed at `p`, the targets gathered afresh: the drag that follows keeps - /// them ([`Self::drag`]). - pub fn press(&mut self, cx: &ToolContext, p: Point, exclude: &[NodeId], from: Option<&Leave>) -> Point { - self.gather(cx, SnapKey::of(cx, exclude)); - self.snap(cx, p, from) - } - - /// Gather `targets` other than the art for the drag a press starts (a point being dragged - /// leaves itself out); the next hover gathers its own again. - pub fn hold(&mut self, cx: &ToolContext, targets: impl FnOnce() -> Targets) { - self.targets = Some((None, PointSnap::new(cx, targets))); - self.guides.clear(); - } - - /// Snap `p`, dragged since the press, with the targets the press gathered. - pub fn drag(&mut self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> Point { - if self.targets.is_none() { - self.gather(cx, SnapKey::of(cx, &[])); - } - self.snap(cx, p, from) - } - - /// The guides of the last snap. - pub fn guides(&self) -> &[Overlay] { - &self.guides - } - - /// Hide the guides (the gesture ended). - pub fn clear(&mut self) { - self.guides.clear(); - } - - fn gather(&mut self, cx: &ToolContext, key: SnapKey) { - let snap = PointSnap::new(cx, || Targets::for_drawing(cx.doc, &key.exclude, visible(cx), key.snap_to_glyph)); - self.targets = Some((Some(key), snap)); - } - - fn snap(&mut self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> Point { - let (q, guides) = match &self.targets { - Some((_, t)) => t.snap_from(cx, p, from), - None => (p, vec![]), - }; - self.guides = guides; - q - } -} - -/// Snapping for a dragged direction handle: Shift keeps it at 45° steps round its anchor, else it -/// snaps as a drawn point does ([`DrawSnap`]). The targets ([`Targets::for_handle`]) are gathered -/// on the first move that needs them and kept for the rest of the drag. -#[derive(Default)] -pub struct HandleSnap(DrawSnap); - -impl HandleSnap { - /// Where the handle of anchor `ai` of subpath `si` of path `id` goes with the pointer at `p`, - /// and the guides showing why. - pub fn snap(&mut self, cx: &ToolContext, (id, si, ai): (NodeId, usize, usize), p: Point, shift: bool) -> (Point, Vec) { - let path = cx.doc.node(id).and_then(|n| n.path_data()); - if shift && let Some(a) = path.and_then(|pd| pd.subpaths.get(si)?.anchors.get(ai)) { - return (a.p + constrain_angle_from(p - a.p, 45.0, cx.constrain_angle), vec![]); - } - if self.0.targets.is_none() { - self.0.hold(cx, || Targets::for_handle(cx.doc, id)); - } - let q = self.0.drag(cx, p, None); - (q, self.0.guides().to_vec()) - } -} - -/// Snap a picked point (a transform tool's reference point) to the nearest anchor or centre of the -/// selection or of the object under the pointer, when Snap to Point or Smart Guides is on. -pub fn snap_pick(cx: &ToolContext, p: Point) -> (Point, Vec) { - if !(cx.snap_to_point || cx.smart_guides) { - return (p, vec![]); - } - let tol = if cx.smart_guides { cx.snap_tol() } else { cx.tol(cx.snap_tolerance) }; - let mut roots = cx.selection.objects.clone(); - roots.extend(hit_test(cx.doc, p, HitOptions { tol, skip_guides: false, ..cx.hit_options() }).map(|h| h.leaf)); - let near = Rect::new(p.x - tol, p.y - tol, p.x + tol, p.y + tol); - Targets::points_near(cx.doc, &roots, near).styled(cx).snap_point(p, tol) -} - -/// Distance Guides (Smart Guides → Invoke Distance Guides): how far the selection's bounds `from` -/// lie from `to` along each axis, as measured lines ([`Overlay::Distance`]) labelled with their -/// lengths in the General unit. Along an axis on which the two lie apart, one line spans the gap -/// between their facing edges; along one on which they overlap, or one holds the other, one line -/// runs between their left (top) edges and one between their right (bottom) edges. An artboard -/// (`artboard`) is always measured edge to edge: the selection's distance from its left, right, -/// top and bottom edges. Each line runs across the middle of where the two overlap on the other -/// axis, else across the selection's middle, with a dashed line from `to`'s edge out to it when it -/// passes beside `to`. Lines of no length are left out. -pub fn distance_guides(cx: &ToolContext, from: Rect, to: Rect, artboard: bool) -> Vec { - let finite = |r: &Rect| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()); - if !finite(&from) || !finite(&to) { - return vec![]; - } - let color = cx.smart_guide_color; - let mut out = Vec::new(); - for x in [true, false] { - let ((f0, f1), (t0, t1)) = (span(&from, x), span(&to, x)); - let ((g0, g1), (u0, u1)) = (span(&from, !x), span(&to, !x)); - let (lo, hi) = (g0.max(u0), g1.min(u1)); - let across = if lo <= hi { (lo + hi) / 2.0 } else { (g0 + g1) / 2.0 }; - let pt = |u: f64, c: f64| if x { Point::new(u, c) } else { Point::new(c, u) }; - // Each measured line: from the selection's edge to `to`'s. - let lines: Vec<(f64, f64)> = match (artboard, f1 <= t0, t1 <= f0) { - (false, true, _) => vec![(f1, t0)], - (false, _, true) => vec![(f0, t1)], - _ => vec![(f0, t0), (f1, t1)], - }; - for (fu, tu) in lines { - let length = (tu - fu).abs(); - if length <= 1e-9 { - continue; - } - out.push(Overlay::Distance { a: pt(fu, across), b: pt(tu, across), text: cx.len(length), color }); - let on_edge = across.clamp(u0, u1); - if on_edge != across { - out.push(Overlay::Line { a: pt(tu, on_edge), b: pt(tu, across), color, dashed: true }); - } - } - } - out -} - #[cfg(test)] mod tests; diff --git a/crates/tools/src/guides/snap.rs b/crates/tools/src/guides/snap.rs new file mode 100644 index 000000000..673e9f078 --- /dev/null +++ b/crates/tools/src/guides/snap.rs @@ -0,0 +1,455 @@ +use super::targets::{Targets, span}; +use super::*; + +/// Where curves `a` and `b` cross near `p`: a point projected from one onto the other and back +/// settles where they meet (none when they don't nearby). +pub(super) fn meeting(a: &PathSeg, b: &PathSeg, p: Point) -> Option { + let mut q = p; + for _ in 0..32 { + let on_a = a.eval(a.nearest(q, 1e-9).t); + q = b.eval(b.nearest(on_a, 1e-9).t); + if on_a.distance(q) <= 1e-6 { + return Some(q); + } + } + None +} + +/// The direction of `seg` at `t` (not normalised; zero in a cusp). +pub(super) fn seg_direction(seg: &PathSeg, t: f64) -> Vec2 { + match seg { + PathSeg::Line(l) => l.p1 - l.p0, + PathSeg::Quad(q) => q.deriv().eval(t).to_vec2(), + PathSeg::Cubic(c) => c.deriv().eval(t).to_vec2(), + } +} + +/// The parameters in (0, 1) where the line from `at` to `seg` is square to it (`perpendicular`) +/// or grazes it, found by sampling the curve for a change of sign and bisecting. +pub(super) fn touch_params(seg: &PathSeg, at: Point, perpendicular: bool) -> Vec { + const STEPS: usize = 24; + let f = |t: f64| { + let (v, d) = (seg.eval(t) - at, seg_direction(seg, t)); + if perpendicular { v.dot(d) } else { v.cross(d) } + }; + let mut out = vec![]; + let mut prev = (0.0, f(0.0)); + for i in 1..=STEPS { + let t = i as f64 / STEPS as f64; + let cur = (t, f(t)); + if prev.1.is_finite() && cur.1.is_finite() && prev.1 * cur.1 < 0.0 { + let (mut lo, mut hi) = (prev, cur); + for _ in 0..40 { + let mid = 0.5 * (lo.0 + hi.0); + let m = (mid, f(mid)); + if lo.1 * m.1 <= 0.0 { + hi = m; + } else { + lo = m; + } + } + let t = 0.5 * (lo.0 + hi.0); + // A jump through a cusp changes sign without crossing zero: the line must really + // run along (or square to) the curve there. + let (v, d) = (seg.eval(t) - at, seg_direction(seg, t)); + let scale = v.hypot() * d.hypot(); + if scale > 1e-12 && f(t).abs() <= 1e-3 * scale { + out.push(t); + } + } + prev = cur; + } + out +} + +/// Where a press at `p` takes hold of `path` by its outline: the point of the path nearest `p`, +/// when it is within `reach`, and on a straight segment exactly on its line. A path dragged by a +/// press on its stroke or beside its edge snaps by that point of the path, so it is the path that +/// lands on a ruler guide or lines up, not the pressed spot a few pixels beside it (#3701). +pub(crate) fn hold_on_path(path: &vectorcraft_geom::PathData, p: Point, reach: f64) -> Option { + let (si, seg, _, q, d) = path.nearest(p)?; + if d > reach { + return None; + } + let sp = path.subpaths.get(si)?; + if !sp.segment_is_line(seg) { + return Some(q); + } + let c = sp.segment(seg); + let line = Line::new(c.p0, c.p3); + Some(line.eval(line.nearest(p, 0.0).t)) +} + +/// Snap `p` for a drawing tool when smart guides (or grid snapping) are on. +pub fn snap_draw(cx: &ToolContext, p: Point, exclude: &[NodeId]) -> (Point, Vec) { + snap_with(cx, p, || Targets::collect(cx.doc, exclude, None)) +} + +/// Snap `p` to pixels or the grid when they are on, else to the smart guide `targets`, else (Snap +/// to Point) to their anchors and the ruler guides within the Snap to Point distance. +fn snap_with(cx: &ToolContext, p: Point, targets: impl FnOnce() -> Targets) -> (Point, Vec) { + PointSnap::new(cx, targets).snap(cx, p) +} + +/// Snapping for a dragged point, its targets gathered once (when the drag begins): what +/// [`snap_draw`] does for a point at a time. +pub struct PointSnap { + /// The smart guide targets, or with Smart Guides off those of Snap to Point, and how near (in + /// screen pixels) they pull; none with pixels or the grid snapping instead, or nothing on. + targets: Option<(Targets, f64)>, +} + +impl PointSnap { + pub fn new(cx: &ToolContext, targets: impl FnOnce() -> Targets) -> Self { + let targets = if cx.snap_to_pixel || cx.snap_to_grid { + None + } else if cx.smart_guides { + // Snap to Point keeps pulling points onto the ruler guides with Smart Guides on (#812). + let mut t = targets().styled(cx); + (t.tangent, t.perpendicular) = (cx.snap_tangent, cx.snap_perpendicular); + Some((if cx.snap_to_point { t.with_rulers(cx) } else { t }, cx.snapping_tolerance)) + } else if cx.snap_to_point { + Some((targets().for_snap_to_point(cx), cx.snap_tolerance)) + } else { + None + }; + Self { targets } + } + + /// Where `p` goes, and the guides showing why. + pub fn snap(&self, cx: &ToolContext, p: Point) -> (Point, Vec) { + self.snap_from(cx, p, None) + } + + /// Where a drawn point `p` leaving `from` goes ([`Targets::snap_from`]), and the guides + /// showing why. Pixels and the grid round it before Shift turns it onto its angle. + pub fn snap_from(&self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> (Point, Vec) { + let keep = |q: Point| from.and_then(|f| f.constrained(q)).unwrap_or(q); + if cx.snap_to_pixel { + return (keep(Point::new(p.x.round(), p.y.round())), vec![]); + } + if cx.snap_to_grid { + return (keep(vectorcraft_geom::snap::snap_point_to_grid(p, cx.grid_step())), vec![]); + } + match &self.targets { + Some((t, tol)) => t.snap_from(p, from, cx.tol(*tol)), + None => (keep(p), vec![]), + } + } + + /// Snap a bounding-box resize ([`Targets::snap_scale`]) as [`Self::snap`] snaps a point. With + /// Snap to Pixel on, the corner or side the handle moves lines up with whole pixels, and with + /// Snap to Grid on it lines up with the nearest gridline, however far that is; neither shows + /// guides. Otherwise the handle snaps to the targets [`Self::new`] gathered. + pub fn snap_scale( + &self, + cx: &ToolContext, + bx: &OrientedBox, + handle: Handle, + a: Affine, + proportional: bool, + from_center: bool, + ) -> (Affine, Vec) { + let step = if cx.snap_to_pixel { + Some(1.0) + } else if cx.snap_to_grid { + Some(cx.grid_step()).filter(|s| s.is_finite() && *s > 0.0) + } else { + None + }; + if let Some(step) = step { + let moved = Handle::ALL.map(|h| bx.to_doc() * (a * h.pos(bx.rect))); + // Every point the handle moves is within half a step of a gridline, so a tolerance of + // one step always finds one. + return (Targets::grid_lines(moved, step).snap_scale(bx, handle, a, proportional, from_center, step).0, vec![]); + } + match &self.targets { + Some((t, tol)) => t.snap_scale(bx, handle, a, proportional, from_center, cx.tol(*tol)), + None => (a, vec![]), + } + } +} + +/// The Construction Guides angle set new preferences start with. +pub const DEFAULT_CONSTRUCTION_ANGLES: &str = "90° & 45° Angles"; + +/// The angles (degrees) of the construction guides of a Smart Guides › Angles set: "90°" gives the +/// horizontal and the vertical, "45°" the diagonals, "60°" and "30°" their multiples between those. +pub fn construction_angles(set: &str) -> &'static [f64] { + match set { + "30° Angles" => &[30.0, 60.0, 120.0, 150.0], + "45° Angles" => &[45.0, 135.0], + "60° Angles" => &[60.0, 120.0], + "90° Angles" => &[0.0, 90.0], + "90° & 45° & 30° Angles" => &[0.0, 30.0, 45.0, 60.0, 90.0, 120.0, 135.0, 150.0], + _ => &[0.0, 45.0, 90.0, 135.0], + } +} + +/// How Shift keeps a drawn point to the point it leaves from. +#[derive(Clone, Copy, Debug, PartialEq)] +enum Keep { + /// At steps of `step` degrees counted from `base` (a segment). + Angle { step: f64, base: f64 }, + /// On a diagonal, as far along as the pointer's farther axis (a square's corner). + Square, +} + +/// The point a drawn point leaves from (the anchor before it, a shape's start) and how it keeps +/// to it: with Shift held at fixed angles round it, and onto the construction guides through it +/// (Smart Guides › Construction Guides). +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Leave { + pub at: Point, + /// Shift held: how the point keeps to `at`. + shift: Option, + /// The angles of the construction guides through `at` (none with them off). + angles: &'static [f64], + /// A drawn segment: it may land square to a segment or grazing a curve (a box does not). + pub(super) contact: bool, +} + +impl Leave { + /// A segment from `at` (Pen, Curvature, Line Segment): Shift keeps it at 45° steps from + /// General › Constrain Angle; with Smart Guides on it lands on the construction guides. + pub fn segment(cx: &ToolContext, at: Point, shift: bool) -> Self { + Self { + at, + shift: shift.then_some(Keep::Angle { step: 45.0, base: cx.constrain_angle }), + angles: if cx.smart_guides { cx.construction_angles } else { &[] }, + contact: true, + } + } + + /// A curved source cannot make a line-to-line perpendicular or line-to-curve tangent contact. + pub fn without_contact(mut self) -> Self { + self.contact = false; + self + } + + /// A box dragged from its corner or centre `at` (Rectangle, Ellipse, Arc, the grids, and + /// Artboard, which passes no Shift): Shift keeps the dragged corner on a diagonal, the box a + /// square. + pub fn diagonal(at: Point, shift: bool) -> Self { + Self { at, shift: shift.then_some(Keep::Square), angles: &[], contact: false } + } + + /// Where Shift puts `p`: none without it. + pub(super) fn constrained(&self, p: Point) -> Option { + let v = p - self.at; + Some(match self.shift? { + Keep::Angle { step, base } => self.at + constrain_angle_from(v, step, base), + Keep::Square => self.at + square(v), + }) + } + + /// The construction guide through `at` nearest `p`, within `tol` of it (and `p` farther than + /// that from `at`, where every guide meets): its direction and where `p` lands on it. + pub(super) fn construction(&self, p: Point, tol: f64) -> Option<(Vec2, Point)> { + let v = p - self.at; + if v.hypot() <= tol { + return None; + } + self.angles + .iter() + // Counter-clockwise as seen on the y-down page. + .map(|a| Vec2::new(a.to_radians().cos(), -a.to_radians().sin())) + .map(|d| (d, self.at + d * v.dot(d))) + .map(|(d, q)| (d, q, q.distance(p))) + .filter(|(.., dist)| *dist <= tol) + .min_by(|a, b| a.2.total_cmp(&b.2)) + .map(|(d, q, _)| (d, q)) + } +} + +/// `d` made square: as long on both axes as on its longer one, each keeping its sign. +pub fn square(d: Vec2) -> Vec2 { + let s = d.x.abs().max(d.y.abs()); + Vec2::new(s * d.x.signum(), s * d.y.signum()) +} + +/// Smart Guides for the points a drawing tool places: the Pen's and Curvature tool's anchors, +/// the Pencil's ends, the shape tools' start and dragged corner, the Type tool's click and the +/// Artboard tool's new artboard. Each such tool keeps one and asks it for every point, hovering +/// too, so they all snap alike, and shows its [`Self::guides`]. +/// +/// The targets ([`Targets::for_drawing`], the art in the window) are gathered once per document +/// state: hovering gathers them again only after the document, the selection, what is left out, +/// the window or the snapping options changed; a press always gathers them afresh, and the drag +/// it starts keeps them, the document then showing the drag's own previews. +#[derive(Default)] +pub struct DrawSnap { + /// The targets, and what they were gathered for (none: for one drag only). + targets: Option<(Option, PointSnap)>, + guides: Vec, +} + +/// What a [`DrawSnap`]'s targets were gathered for: the document state, the art left out, the +/// window and every option [`PointSnap::new`] reads. +#[derive(PartialEq)] +struct SnapKey { + revision: (u64, u64), + exclude: Vec, + screen: Option, + view: [bool; 7], + snap_to_glyph: bool, + style: GuideStyle, + tolerances: [f64; 2], +} + +impl SnapKey { + fn of(cx: &ToolContext, exclude: &[NodeId]) -> Self { + Self { + revision: cx.revision, + exclude: exclude.to_vec(), + screen: cx.screen, + view: [cx.smart_guides, cx.snap_to_point, cx.snap_to_pixel, cx.snap_to_grid, cx.guides, cx.snap_tangent, cx.snap_perpendicular], + snap_to_glyph: cx.snap_to_glyph && cx.alignment_guides, + style: GuideStyle::of(cx), + tolerances: [cx.snapping_tolerance, cx.snap_tolerance], + } + } +} + +/// The part of the document the window shows (none headless: all of it). +fn visible(cx: &ToolContext) -> Option { + let s = cx.screen?; + let (w, h) = s.size; + Some(Rect::from_points(s.at(0.0, 0.0), s.at(w, h)).union_pt(s.at(w, 0.0)).union_pt(s.at(0.0, h))) +} + +impl DrawSnap { + /// Snap the pointer hovering at `p` (the art but `exclude` as targets, the anchors of + /// `exclude` still pulling), reusing the targets while nothing they depend on changed. + pub fn hover(&mut self, cx: &ToolContext, p: Point, exclude: &[NodeId], from: Option<&Leave>) -> Point { + let key = SnapKey::of(cx, exclude); + if self.targets.as_ref().is_none_or(|(k, _)| k.as_ref() != Some(&key)) { + self.gather(cx, key); + } + self.snap(cx, p, from) + } + + /// Snap the point pressed at `p`, the targets gathered afresh: the drag that follows keeps + /// them ([`Self::drag`]). + pub fn press(&mut self, cx: &ToolContext, p: Point, exclude: &[NodeId], from: Option<&Leave>) -> Point { + self.gather(cx, SnapKey::of(cx, exclude)); + self.snap(cx, p, from) + } + + /// Gather `targets` other than the art for the drag a press starts (a point being dragged + /// leaves itself out); the next hover gathers its own again. + pub fn hold(&mut self, cx: &ToolContext, targets: impl FnOnce() -> Targets) { + self.targets = Some((None, PointSnap::new(cx, targets))); + self.guides.clear(); + } + + /// Snap `p`, dragged since the press, with the targets the press gathered. + pub fn drag(&mut self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> Point { + if self.targets.is_none() { + self.gather(cx, SnapKey::of(cx, &[])); + } + self.snap(cx, p, from) + } + + /// The guides of the last snap. + pub fn guides(&self) -> &[Overlay] { + &self.guides + } + + /// Hide the guides (the gesture ended). + pub fn clear(&mut self) { + self.guides.clear(); + } + + fn gather(&mut self, cx: &ToolContext, key: SnapKey) { + let snap = PointSnap::new(cx, || Targets::for_drawing(cx.doc, &key.exclude, visible(cx), key.snap_to_glyph)); + self.targets = Some((Some(key), snap)); + } + + fn snap(&mut self, cx: &ToolContext, p: Point, from: Option<&Leave>) -> Point { + let (q, guides) = match &self.targets { + Some((_, t)) => t.snap_from(cx, p, from), + None => (p, vec![]), + }; + self.guides = guides; + q + } +} + +/// Snapping for a dragged direction handle: Shift keeps it at 45° steps round its anchor, else it +/// snaps as a drawn point does ([`DrawSnap`]). The targets ([`Targets::for_handle`]) are gathered +/// on the first move that needs them and kept for the rest of the drag. +#[derive(Default)] +pub struct HandleSnap(pub(crate) DrawSnap); + +impl HandleSnap { + /// Where the handle of anchor `ai` of subpath `si` of path `id` goes with the pointer at `p`, + /// and the guides showing why. + pub fn snap(&mut self, cx: &ToolContext, (id, si, ai): (NodeId, usize, usize), p: Point, shift: bool) -> (Point, Vec) { + let path = cx.doc.node(id).and_then(|n| n.path_data()); + if shift && let Some(a) = path.and_then(|pd| pd.subpaths.get(si)?.anchors.get(ai)) { + return (a.p + constrain_angle_from(p - a.p, 45.0, cx.constrain_angle), vec![]); + } + if self.0.targets.is_none() { + self.0.hold(cx, || Targets::for_handle(cx.doc, id)); + } + let q = self.0.drag(cx, p, None); + (q, self.0.guides().to_vec()) + } +} + +/// Snap a picked point (a transform tool's reference point) to the nearest anchor or centre of the +/// selection or of the object under the pointer, when Snap to Point or Smart Guides is on. +pub fn snap_pick(cx: &ToolContext, p: Point) -> (Point, Vec) { + if !(cx.snap_to_point || cx.smart_guides) { + return (p, vec![]); + } + let tol = if cx.smart_guides { cx.snap_tol() } else { cx.tol(cx.snap_tolerance) }; + let mut roots = cx.selection.objects.clone(); + roots.extend(hit_test(cx.doc, p, HitOptions { tol, skip_guides: false, ..cx.hit_options() }).map(|h| h.leaf)); + let near = Rect::new(p.x - tol, p.y - tol, p.x + tol, p.y + tol); + Targets::points_near(cx.doc, &roots, near).styled(cx).snap_point(p, tol) +} + +/// Distance Guides (Smart Guides → Invoke Distance Guides): how far the selection's bounds `from` +/// lie from `to` along each axis, as measured lines ([`Overlay::Distance`]) labelled with their +/// lengths in the General unit. Along an axis on which the two lie apart, one line spans the gap +/// between their facing edges; along one on which they overlap, or one holds the other, one line +/// runs between their left (top) edges and one between their right (bottom) edges. An artboard +/// (`artboard`) is always measured edge to edge: the selection's distance from its left, right, +/// top and bottom edges. Each line runs across the middle of where the two overlap on the other +/// axis, else across the selection's middle, with a dashed line from `to`'s edge out to it when it +/// passes beside `to`. Lines of no length are left out. +pub fn distance_guides(cx: &ToolContext, from: Rect, to: Rect, artboard: bool) -> Vec { + let finite = |r: &Rect| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()); + if !finite(&from) || !finite(&to) { + return vec![]; + } + let color = cx.smart_guide_color; + let mut out = Vec::new(); + for x in [true, false] { + let ((f0, f1), (t0, t1)) = (span(&from, x), span(&to, x)); + let ((g0, g1), (u0, u1)) = (span(&from, !x), span(&to, !x)); + let (lo, hi) = (g0.max(u0), g1.min(u1)); + let across = if lo <= hi { (lo + hi) / 2.0 } else { (g0 + g1) / 2.0 }; + let pt = |u: f64, c: f64| if x { Point::new(u, c) } else { Point::new(c, u) }; + // Each measured line: from the selection's edge to `to`'s. + let lines: Vec<(f64, f64)> = match (artboard, f1 <= t0, t1 <= f0) { + (false, true, _) => vec![(f1, t0)], + (false, _, true) => vec![(f0, t1)], + _ => vec![(f0, t0), (f1, t1)], + }; + for (fu, tu) in lines { + let length = (tu - fu).abs(); + if length <= 1e-9 { + continue; + } + out.push(Overlay::Distance { a: pt(fu, across), b: pt(tu, across), text: cx.len(length), color }); + let on_edge = across.clamp(u0, u1); + if on_edge != across { + out.push(Overlay::Line { a: pt(tu, on_edge), b: pt(tu, across), color, dashed: true }); + } + } + } + out +} diff --git a/crates/tools/src/guides/targets.rs b/crates/tools/src/guides/targets.rs new file mode 100644 index 000000000..4d77ac652 --- /dev/null +++ b/crates/tools/src/guides/targets.rs @@ -0,0 +1,1096 @@ +use super::snap::{Leave, meeting, seg_direction, touch_params}; +use super::*; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(super) enum Kind { + Anchor, + Center, + Edge, + Artboard, + Bleed, + Guide, + Glyph, +} + +impl Kind { + fn label(self) -> &'static str { + match self { + Kind::Anchor => "anchor", + Kind::Center => "center", + Kind::Edge => "path", + Kind::Artboard => "artboard", + Kind::Bleed => "bleed", + Kind::Guide => "guide", + Kind::Glyph => "glyph", + } + } +} + +/// Snap targets gathered from the document (excluding the objects being edited). +#[derive(Default)] +pub struct Targets { + pub(super) points: Vec<(Point, Kind)>, + pub(super) xs: Vec<(f64, Point, Kind)>, + pub(super) ys: Vec<(f64, Point, Kind)>, + /// Path segments and their control bounds, which a point lands on ("path"): gathered only for + /// dragged anchors ([`Self::for_anchor_drag`]). + pub(super) segments: Vec<(Rect, PathSeg)>, + /// Ruler guides, which pull a point into line where they run (Snap to Point). + pub(super) rulers: Vec, + /// The bounds of the objects a moved selection spaces itself evenly with (Spacing Guides): + /// gathered only for moves ([`Self::for_move`]). + pub(super) boxes: Vec, + /// Line Extension: the lines a single dragged anchor keeps to, along the straight segments + /// it ends, as the end that stays put and the unit direction towards the dragged one + /// ([`Self::for_anchor_drag`]). + pub(super) extensions: Vec<(Point, Vec2)>, + /// Snap to Tangent and Snap to Perpendicular: a drawn line from an anchor lands where it + /// grazes a curve, or meets a segment square ([`Self::touching`]). + pub(super) tangent: bool, + pub(super) perpendicular: bool, + /// How the guides these targets produce look. + pub(super) style: GuideStyle, +} + +/// How many anchors, and how many segments, the targets gather at most. +const BUDGET: usize = 20_000; + +/// What [`Targets::collect_inner`] gathers from each path besides its anchors and bounds. +#[derive(Clone, Copy, PartialEq)] +enum Gather { + /// Nothing more: a moved or resized selection lines up with bounds. + Bounds, + /// Its segments, which a dragged anchor lands on. + Segments, + /// Its segments, and its anchors as lines too: a drawn point lines up with them. + Drawing, +} + +/// The entries of `sorted` (ascending by `key`) whose key is within `tol` of `v`, found by +/// bisection. +fn within(sorted: &[T], key: impl Fn(&T) -> f64, v: f64, tol: f64) -> &[T] { + let lo = sorted.partition_point(|e| key(e) < v - tol); + let hi = sorted.partition_point(|e| key(e) <= v + tol); + sorted.get(lo..hi).unwrap_or_default() +} + +/// Visit `nodes` and everything inside them, but the nodes in `skip` and everything inside those: +/// one pass, however many are skipped. A Shaper Group's original shapes, its first child, are +/// not shown and are skipped too. +fn walk_but<'a>(nodes: &'a [Arc], skip: &HashSet, f: &mut impl FnMut(&'a Node)) { + for n in nodes.iter().filter(|n| !skip.contains(&n.id)) { + f(n); + if let Some(ch) = n.children() { + walk_but(ch.get(usize::from(n.extras().shaper.is_some())..).unwrap_or_default(), skip, f); + } + } +} + +/// The lines of `lines` (sorted by where they run) within `tol` of `v`. +pub(super) fn lines_near(lines: &[(f64, Point, Kind)], v: f64, tol: f64) -> &[(f64, Point, Kind)] { + within(lines, |l| l.0, v, tol) +} + +/// `r`'s extent along the x axis (`x`) or the y axis. +pub(super) fn span(r: &Rect, x: bool) -> (f64, f64) { + if x { (r.x0, r.x1) } else { (r.y0, r.y1) } +} + +/// Along axis `x`, does dragging `handle` move the box's minimum edge (true) or maximum (false)? +fn resize_moves_min_edge(handle: Handle, x: bool) -> Option { + let (ax, ay) = handle.axes(); + if x && !ax || !x && !ay { + return None; + } + if x { + Some(matches!(handle, Handle::Left | Handle::TopLeft | Handle::BottomLeft)) + } else { + Some(matches!(handle, Handle::Top | Handle::TopLeft | Handle::TopRight)) + } +} + +/// The objects a moved selection (`moving`) spaces itself evenly with, by their bounds: each +/// visible object on a layer as a whole (a group too), and inside a group holding some of the +/// moving objects its other objects, but a Shaper Group's original shapes. At most [`BUDGET`]. +pub(super) fn object_boxes(doc: &Document, moving: &[NodeId]) -> Vec { + fn push(nodes: &[Arc], moving: &HashSet, holders: &HashSet, out: &mut Vec) { + for n in nodes.iter().filter(|n| n.visible && !moving.contains(&n.id)) { + if out.len() >= BUDGET { + return; + } + match n.children() { + Some(ch) if n.is_layer() || holders.contains(&n.id) => { + push(ch.get(usize::from(n.extras().shaper.is_some())..).unwrap_or_default(), moving, holders, out) + } + _ => out.extend(n.geometric_bounds()), + } + } + } + let holders: HashSet = moving.iter().filter_map(|id| doc.ancestry(*id)).flatten().collect(); + let mut out = vec![]; + push(&doc.layers, &moving.iter().copied().collect(), &holders, &mut out); + out +} + +impl Targets { + pub fn collect(doc: &Document, exclude: &[NodeId], visible: Option) -> Self { + Self::collect_inner(doc, exclude, None, visible, Gather::Bounds) + } + + /// Targets for moving the selection of `cx`: [`Self::collect`]'s but `exclude` (the selection, + /// or nothing for a copy, which leaves it behind), styled as `cx` asks, and with Smart Guides › + /// Spacing Guides the objects it spaces itself evenly with ([`Self::snap_rect`]). + pub fn for_move(cx: &ToolContext, exclude: &[NodeId]) -> Self { + let mut t = Self::collect(cx.doc, exclude, None).styled(cx); + if cx.spacing_guides { + t.boxes = object_boxes(cx.doc, &cx.selection.objects); + } + t + } + + /// Targets for scaling the selection: the other art, styled as `cx` asks, and with Smart Guides › + /// Spacing Guides the neighbours whose gaps the moving edge should match ([`Self::snap_scale`]). + pub fn for_scale(cx: &ToolContext) -> Self { + Self::for_move(cx, &cx.selection.objects) + } + + /// Targets for a drawn point: the art in `visible` (all of it for None) but `exclude`, its + /// anchors, centres, edges and path segments, its anchors in line too. The anchors and + /// segments of `exclude` (the path being drawn, whose bounds grow with it) stay targets. + pub fn for_drawing(doc: &Document, exclude: &[NodeId], visible: Option, glyphs: bool) -> Self { + let mut t = Self::collect_inner(doc, exclude, None, visible, Gather::Drawing); + t.push_paths(doc, exclude, true); + if glyphs { + t.push_glyphs(doc, exclude, visible); + } + t.index() + } + + /// Targets for a dragged direction handle of path `id`: the other art, and the anchors of `id` + /// (its bounds and the segments through the handle move with it, so they would chase it). + pub fn for_handle(doc: &Document, id: NodeId) -> Self { + let mut t = Self::collect_inner(doc, &[id], None, None, Gather::Bounds); + t.push_paths(doc, &[id], false); + t.index() + } + + /// The anchors of the paths `ids` as targets, on them and in line, and their segments too + /// when `segments`: at most [`BUDGET`] of each. + fn push_paths(&mut self, doc: &Document, ids: &[NodeId], segments: bool) { + let (mut anchors, mut segment_budget) = (BUDGET, if segments { BUDGET } else { 0 }); + for path in ids.iter().filter_map(|id| doc.node(*id)?.path_data()) { + for sp in &path.subpaths { + for a in sp.anchors.iter().take(anchors) { + self.push_anchor(a.p); + anchors -= 1; + } + self.push_segments(sp, |_| false, &mut segment_budget); + } + } + } + + /// Sort the targets so that [`within`] finds those near a point by bisection: the anchors + /// and centres by x, the lines by where they run. Every constructor ends with it (the sort + /// is stable: of targets alike, the first gathered still wins). + fn index(mut self) -> Self { + self.points.sort_by(|a, b| a.0.x.total_cmp(&b.0.x)); + self.xs.sort_by(|a, b| a.0.total_cmp(&b.0)); + self.ys.sort_by(|a, b| a.0.total_cmp(&b.0)); + self + } + + /// Anchor `a` as a target: a point lands on it and lines up with it. + fn push_anchor(&mut self, a: Point) { + self.points.push((a, Kind::Anchor)); + self.xs.push((a.x, a, Kind::Anchor)); + self.ys.push((a.y, a, Kind::Anchor)); + } + + /// Live-text baselines and glyph bounds (View › Snap to Glyph). + fn push_glyphs(&mut self, doc: &Document, exclude: &[NodeId], visible: Option) { + let skip: HashSet = exclude.iter().copied().collect(); + let db = FontDb::global(); + let kind = Kind::Glyph; + let mut budget = BUDGET / 4; + walk_but(&doc.layers, &skip, &mut |n| { + if budget == 0 || !n.visible { + return; + } + let NodeKind::Text(t) = &n.kind else { return }; + let lay = layout_text(db, t); + let ink = t.xf.transform_rect_bbox(lay.bounds); + if !ink.is_finite() || ink.width() <= 0.0 || ink.height() <= 0.0 { + return; + } + if let Some(v) = visible + && ink.intersect(v).area() <= 0.0 + && !v.contains(ink.center()) + { + return; + } + let to_doc = |p: Point| t.xf * p; + for line in &lay.lines { + let at = to_doc(Point::new(line.x0, line.baseline)); + self.ys.push((at.y, at, kind)); + } + for g in &lay.glyphs { + if budget == 0 { + break; + } + let bb = g.outline.bounding_box(); + if !(bb.width() > 0.0 && bb.height() > 0.0) { + continue; + } + let r = t.xf.transform_rect_bbox(bb); + for p in [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)] { + self.xs.push((p.x, p, kind)); + self.ys.push((p.y, p, kind)); + } + budget -= 1; + } + }); + } + + /// The same targets drawing their guides as the Smart Guides preferences in `cx` ask + /// ([`GuideStyle::of`]). + pub fn styled(mut self, cx: &ToolContext) -> Self { + self.style = GuideStyle::of(cx); + self + } + + /// An alignment line from `a` to `b`, unless Alignment Guides are off. + fn line(&self, a: Point, b: Point) -> Option { + self.style.lines.then_some(Overlay::Line { a, b, color: self.style.color, dashed: false }) + } + + /// A label `text` at `p`, unless Anchor/Path Labels are off. + fn label(&self, p: Point, text: &str) -> Option { + self.style.labels.then(|| Overlay::Label { p, text: text.into(), color: self.style.color }) + } + + /// Targets for dragging artboard `index`: everything except that artboard and `exclude` (the + /// art that moves along with it). + pub fn for_artboard(doc: &Document, index: usize, exclude: &[NodeId]) -> Self { + Self::collect_inner(doc, exclude, Some(index), None, Gather::Bounds) + } + + /// Targets for dragging the direct-selected anchors of `sel` (and its objects selected as a + /// whole): the other art, its path segments too, and the anchors and segments that stay put on + /// the paths being reshaped (their bounds move along, so they are no targets). A single anchor + /// also keeps to the extensions of the straight segments it ends (#2120). + pub fn for_anchor_drag(doc: &Document, sel: &Selection) -> Self { + let mut t = Self::collect_inner(doc, &sel.objects, None, None, Gather::Segments); + let (mut anchors, mut segments) = (BUDGET, BUDGET); + // With several anchors moving, the one under the pointer is not known here, and the + // others' extensions don't run through it. + let single = sel.objects.len() <= 1 && sel.anchors.values().map(|s| s.len()).sum::() == 1; + for (id, set) in &sel.anchors { + let Some(path) = doc.node(*id).and_then(|n| n.path_data()) else { continue }; + for (si, sp) in path.subpaths.iter().enumerate() { + let moving = |ai: usize| set.contains(&(si, ai)); + for a in sp.anchors.iter().enumerate().filter(|(ai, _)| !moving(*ai)).map(|(_, a)| a.p).take(anchors) { + t.push_anchor(a); + anchors -= 1; + } + t.push_segments(sp, moving, &mut segments); + if single { + t.push_extensions(sp, moving); + } + } + } + t.index() + } + + /// The extensions of the straight segments of `sp` with one end among the anchors `moving` + /// accepts and the other staying put. Horizontal and vertical ones are left out: lining up + /// with the other end already keeps the point on them, and says "align". + fn push_extensions(&mut self, sp: &SubPath, moving: impl Fn(usize) -> bool) { + let n = sp.anchors.len(); + for seg in (0..sp.segment_count()).filter(|&seg| moving(seg) != moving((seg + 1) % n) && sp.segment_is_line(seg)) { + let c = sp.segment(seg); + let (fixed, dragged) = if moving(seg) { (c.p3, c.p0) } else { (c.p0, c.p3) }; + let d = dragged - fixed; + let len = d.hypot(); + if len.is_finite() && d.x.abs() > 1e-9 * len && d.y.abs() > 1e-9 * len { + self.extensions.push((fixed, d / len)); + } + } + } + + /// The segments of `sp` that stay put while the anchors `moving` accepts move, as targets, at + /// most `budget` of them (counted down). + fn push_segments(&mut self, sp: &SubPath, moving: impl Fn(usize) -> bool, budget: &mut usize) { + let n = sp.anchors.len(); + for seg in 0..sp.segment_count() { + if *budget == 0 { + return; + } + if moving(seg) || moving((seg + 1) % n) { + continue; + } + let c = sp.segment(seg); + let bounds = Rect::from_points(c.p0, c.p1).union_pt(c.p2).union_pt(c.p3); + // A straight one as a line, so the point lands on it exactly. + self.segments.push((bounds, if sp.segment_is_line(seg) { PathSeg::Line(Line::new(c.p0, c.p3)) } else { PathSeg::Cubic(c) })); + *budget -= 1; + } + } + + fn collect_inner(doc: &Document, exclude: &[NodeId], skip_artboard: Option, visible: Option, gather: Gather) -> Self { + let mut t = Targets::default(); + let add_rect = |t: &mut Targets, r: Rect, kind: Kind| { + let c = r.center(); + for p in [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)] { + t.xs.push((p.x, p, kind)); + t.ys.push((p.y, p, kind)); + } + // A bleed shares its artboard's centre. + if kind != Kind::Bleed { + t.points.push((c, Kind::Center)); + t.xs.push((c.x, c, Kind::Center)); + t.ys.push((c.y, c, Kind::Center)); + } + }; + for (_, ab) in doc.artboards.iter().enumerate().filter(|(i, _)| Some(*i) != skip_artboard) { + add_rect(&mut t, ab.rect, Kind::Artboard); + if doc.setup.has_bleed() { + add_rect(&mut t, doc.setup.bleed_rect(ab.rect), Kind::Bleed); + } + } + let (mut budget, mut segment_budget) = (BUDGET, BUDGET); + walk_but(&doc.layers, &exclude.iter().copied().collect(), &mut |n| { + if budget == 0 || n.is_container() || !n.visible { + return; + } + let Some(b) = n.geometric_bounds() else { return }; + if let Some(v) = visible + && b.intersect(v).area() <= 0.0 + && !v.contains(b.center()) + { + return; + } + if let NodeKind::Path { path, .. } = &n.kind { + for (_, _, a) in path.anchors() { + if gather == Gather::Drawing { + t.push_anchor(a.p); + } else { + t.points.push((a.p, Kind::Anchor)); + } + budget = budget.saturating_sub(1); + } + if gather != Gather::Bounds { + for sp in &path.subpaths { + t.push_segments(sp, |_| false, &mut segment_budget); + } + } + } + add_rect(&mut t, b, Kind::Edge); + }); + t.index() + } + + /// Only these targets' anchors pull (View → Snap to Point). + pub(crate) fn anchors_only(mut self) -> Self { + self.points.retain(|(_, k)| *k == Kind::Anchor); + self.xs.clear(); + self.ys.clear(); + self.segments.clear(); + self.extensions.clear(); + self + } + + /// View → Snap to Point: only these targets' anchors pull, and the ruler guides (shown and + /// unlocked, as the selection tools pick them) pull the pointer into line where they run. + pub(super) fn for_snap_to_point(self, cx: &ToolContext) -> Self { + self.anchors_only().with_rulers(cx) + } + + /// These targets and the ruler guides, when they are shown (and so unlocked to the selection + /// tools): they pull a point into line where they run. + pub(super) fn with_rulers(mut self, cx: &ToolContext) -> Self { + if cx.guides { + self.rulers = cx + .doc + .guides + .iter() + .filter(|g| cx.doc.guide_shown(g)) + .map(|g| Ruler { vertical: g.vertical, pos: g.pos, span: cx.doc.guide_span(g) }) + .collect(); + } + self + } + + /// The ruler guides alone ([`Self::with_rulers`]): what a dragged selection's grabbed point + /// still snaps to with View → Snap to Point when Smart Guides line the selection up. + pub(crate) fn rulers(cx: &ToolContext) -> Self { + Self::default().styled(cx).with_rulers(cx) + } + + /// What a dragged selection's grabbed point snaps to with View → Snap to Point and Smart + /// Guides off: the anchors of the art but `exclude` and the ruler guides. + pub fn snap_to_point(cx: &ToolContext, exclude: &[NodeId]) -> Option { + (cx.snap_to_point && !cx.smart_guides).then(|| Self::collect(cx.doc, exclude, None).for_snap_to_point(cx)) + } + + /// The gridlines (one every `step` from the page's origin, as + /// [`vectorcraft_geom::snap::snap_to_grid`] places them) at and either side of the nearest one + /// to each of `points`, as lines to line up with. + pub(super) fn grid_lines(points: impl IntoIterator, step: f64) -> Self { + let mut t = Targets::default(); + let line = |v: f64, k: i32| vectorcraft_geom::snap::snap_to_grid(v, step, 0.0) + f64::from(k) * step; + for p in points { + for k in -1..=1 { + let (x, y) = (line(p.x, k), line(p.y, k)); + t.xs.push((x, Point::new(x, p.y), Kind::Guide)); + t.ys.push((y, Point::new(p.x, y), Kind::Guide)); + } + } + t.index() + } + + /// Anchors and centres of the visible leaves under `roots` (the roots included) whose bounds + /// reach into `near`. + pub(super) fn points_near(doc: &Document, roots: &[NodeId], near: Rect) -> Self { + let mut t = Targets::default(); + for n in roots.iter().filter_map(|id| doc.node(*id)) { + crate::draw2::walk_shown(n, &mut |c| { + if c.is_container() || !c.visible { + return; + } + let Some(b) = c.geometric_bounds() else { return }; + if b.x0 > near.x1 || b.x1 < near.x0 || b.y0 > near.y1 || b.y1 < near.y0 { + return; + } + if let NodeKind::Path { path, .. } = &c.kind { + t.points.extend(path.anchors().map(|(_, _, a)| (a.p, Kind::Anchor))); + } + t.points.push((b.center(), Kind::Center)); + }); + } + t.index() + } + + /// Snap a single point: onto the nearest anchor or centre, else onto the nearest segment, else + /// into line with targets. Returns the snapped point and guide overlays. + pub fn snap_point(&self, p: Point, tol: f64) -> (Point, Vec) { + self.snap_from(p, None, tol) + } + + /// Snap a drawn point `p` as [`Self::snap_point`] does, and when it leaves `from` (the anchor + /// before it, a shape's start): with Shift held it keeps to its angles round it, sliding along + /// that way into line with the nearest target; else, before lining up, it lands on a + /// construction guide through it, where a target's line crosses the guide if one does nearby. + pub fn snap_from(&self, p: Point, from: Option<&Leave>, tol: f64) -> (Point, Vec) { + if let Some(f) = from + && let Some(q) = f.constrained(p) + { + return match self.slide(f.at, q - f.at, q, tol, true) { + Some((q, target)) => (q, self.lined_up(q, target)), + None => (q, vec![]), + }; + } + if let Some((q, k)) = self.nearest_point(p, tol) { + return (q, self.label(q, k.label()).into_iter().collect()); + } + if let Some(q) = self.crossing(p, tol) { + return (q, self.label(q, "intersect").into_iter().collect()); + } + if let Some(f) = from.filter(|f| f.contact) + && let Some((q, perpendicular, dir)) = self.touching(f.at, p, tol) + { + return (q, self.touch_guides(f.at, q, perpendicular, dir, tol)); + } + if let Some(q) = self.on_segment(p, tol) { + return (q, self.label(q, Kind::Edge.label()).into_iter().collect()); + } + // A construction guide through the point it leaves, else a dragged segment's extension: + // it lands on the guide, sliding into line with a target crossing it nearby. + if let Some((at, dir, q)) = from.and_then(|f| f.construction(p, tol).map(|(dir, q)| (f.at, dir, q))).or_else(|| self.extension(p, tol)) { + let construction = |q: Point| Overlay::Line { a: at, b: q, color: self.style.color, dashed: false }; + return match self.slide(at, dir, q, tol, false) { + Some((q, target)) => (q, std::iter::once(construction(q)).chain(self.lined_up(q, target)).collect()), + None => (q, vec![construction(q)]), + }; + } + let mut out = p; + let mut ov = vec![]; + // The targets' lines along one axis and the ruler guides running past `p`, the nearest + // within reach of `v`. + let nearest = |lines: &[(f64, Point, Kind)], vertical: bool, v: f64| { + let rulers = self.rulers.iter().filter(|r| r.vertical == vertical).filter_map(|r| r.line_at(p, tol)); + lines_near(lines, v, tol) + .iter() + .copied() + .chain(rulers) + .filter(|(t, ..)| (t - v).abs() <= tol) + .min_by(|a, b| (a.0 - v).abs().total_cmp(&(b.0 - v).abs())) + }; + let mut aligned = false; + if let Some((x, from, _)) = nearest(&self.xs, true, p.x) { + out.x = x; + aligned = true; + ov.extend(self.line(from, Point::new(x, p.y))); + } + if let Some((y, from, _)) = nearest(&self.ys, false, p.y) { + out.y = y; + aligned = true; + ov.extend(self.line(from, Point::new(p.x, y))); + } + if aligned { + ov.extend(self.label(out, "align")); + } + (out, ov) + } + + /// Where a line from `at` through a point near `p` touches a target segment: square to it + /// (`perpendicular`, the foot of the perpendicular from `at`) or just grazing a curve + /// (`tangent`), within `tol` of `p`. The point, whether it is the perpendicular one, and the + /// segment's direction there. Targets gather segments for drawn points and dragged anchors only. + pub(super) fn touching(&self, at: Point, p: Point, tol: f64) -> Option<(Point, bool, Vec2)> { + if !(self.tangent || self.perpendicular) || ![at.x, at.y, p.x, p.y, tol].iter().all(|v| v.is_finite()) { + return None; + } + let mut best: Option<(f64, Point, bool, Vec2)> = None; + // Every segment in reach is tried: their order is the document's, not the distance. + for (_, seg) in self.segments.iter().filter(|(b, _)| b.inflate(tol, tol).contains(p)) { + let curved = !matches!(seg, PathSeg::Line(_)); + for (perpendicular, wanted) in [(true, self.perpendicular && !curved), (false, self.tangent && curved)] { + if !wanted { + continue; + } + for t in touch_params(seg, at, perpendicular) { + let (q, d) = (seg.eval(t), seg_direction(seg, t)); + let dist = q.distance(p); + if dist <= tol && q.distance(at) > tol && best.is_none_or(|b| dist < b.0) { + best = Some((dist, q, perpendicular, d)); + } + } + } + } + best.map(|(_, q, perpendicular, d)| (q, perpendicular, d)) + } + + /// The guides of a point landed where the line from `at` touches a segment at `q` running + /// along `dir`: the line itself, a square corner mark for the perpendicular, and the label. + fn touch_guides(&self, at: Point, q: Point, perpendicular: bool, dir: Vec2, tol: f64) -> Vec { + let mut out: Vec = self.line(at, q).into_iter().collect(); + let (u, v) = (at - q, dir); + if perpendicular && self.style.lines && u.hypot2() > 1e-18 && v.hypot2() > 1e-18 { + let (u, v) = (u / u.hypot(), v / v.hypot()); + let s = tol * 1.5; + let mut mark = BezPath::new(); + mark.move_to(q + u * s); + mark.line_to(q + u * s + v * s); + mark.line_to(q + v * s); + out.push(Overlay::Path { path: mark, color: self.style.color, width: 1.0, dashed: false }); + } + out.extend(self.label(q, if perpendicular { "perpendicular" } else { "tangent" })); + out + } + + /// The segment extension nearest `p`, within `tol` of it (and `p` farther than that from the + /// end it starts at): that end, its direction and where `p` lands on it. Where `p` is nearer + /// the horizontal or the vertical through that end there is none: lining up with it ("align") + /// is what straightens a segment that is nearly level or upright. + fn extension(&self, p: Point, tol: f64) -> Option<(Point, Vec2, Point)> { + self.extensions + .iter() + .filter(|(at, _)| at.distance(p) > tol) + .map(|&(at, d)| (at, d, at + d * (p - at).dot(d))) + .map(|(at, d, q)| (at, d, q, q.distance(p))) + .filter(|(at, .., dist)| *dist <= tol && (p.x - at.x).abs().min((p.y - at.y).abs()) > *dist) + .min_by(|a, b| a.3.total_cmp(&b.3)) + .map(|(at, d, q, _)| (at, d, q)) + } + + /// The anchor or centre nearest `p`, within `tol`. + /// Is a target point (an anchor, a centre) within `tol` of `p`? + pub(crate) fn has_point_near(&self, p: Point, tol: f64) -> bool { + self.nearest_point(p, tol).is_some() + } + + pub(super) fn nearest_point(&self, p: Point, tol: f64) -> Option<(Point, Kind)> { + within(&self.points, |q| q.0.x, p.x, tol) + .iter() + .copied() + .filter(|(q, _)| q.distance(p) <= tol) + .min_by(|a, b| a.0.distance(p).total_cmp(&b.0.distance(p))) + } + + /// Slide `q`, on the line through `at` along `dir` (beyond `at` only, for a `ray`), to where + /// the nearest target line crosses it, at most twice `tol` away: where it lands and the + /// target's own point (where its line starts). + fn slide(&self, at: Point, dir: Vec2, q: Point, tol: f64, ray: bool) -> Option<(Point, Point)> { + if !dir.hypot2().is_finite() || dir.hypot2() <= 1e-18 { + return None; + } + let reach = 2.0 * tol; + let mut best: Option<(f64, Point, Point)> = None; + for (vertical, lines, d, a, v) in [(true, &self.xs, dir.x, at.x, q.x), (false, &self.ys, dir.y, at.y, q.y)] { + if d.abs() <= 1e-9 { + continue; + } + // The ruler guides that way count as lines too (#812). + let rulers: Vec<(f64, Point, Kind)> = self.rulers.iter().filter(|r| r.vertical == vertical).filter_map(|r| r.line_at(q, reach)).collect(); + // A line that far across the way crosses it at least that far along. + for (t, from, _) in lines_near(lines, v, reach).iter().chain(&rulers) { + let s = (t - a) / d; + // Exactly on the line, whatever the rounding along the way. + let hit = if vertical { Point::new(*t, at.y + dir.y * s) } else { Point::new(at.x + dir.x * s, *t) }; + let moved = hit.distance(q); + if moved <= reach && (!ray || s > 0.0) && best.is_none_or(|b| moved < b.0) { + best = Some((moved, hit, *from)); + } + } + } + best.map(|(_, hit, from)| (hit, from)) + } + + /// The guides of a point `q` slid into line with the target at `from`: the line from it and + /// "align", or the target's own label when `q` sits right on it. + fn lined_up(&self, q: Point, from: Point) -> Vec { + if let Some((on, k)) = self.nearest_point(q, 1e-9) { + return self.label(on, k.label()).into_iter().collect(); + } + self.line(from, q).into_iter().chain(self.label(q, "align")).collect() + } + + /// The nearest point within `tol` of `p` on a target segment. + fn on_segment(&self, p: Point, tol: f64) -> Option { + self.segments + .iter() + .filter(|(b, _)| b.inflate(tol, tol).contains(p)) + .map(|(_, c)| (c, c.nearest(p, 1e-6))) + .filter(|(_, n)| n.distance_sq <= tol * tol) + .min_by(|a, b| a.1.distance_sq.total_cmp(&b.1.distance_sq)) + .map(|(c, n)| c.eval(n.t)) + } + + /// Where two target paths, a path and a ruler guide, or two ruler guides cross, the crossing + /// nearest `p` within `tol` ("intersect", #1602). + fn crossing(&self, p: Point, tol: f64) -> Option { + // A ruler guide running past, as a short line through the reach of `p`. + let ruler = |r: &Ruler| { + let (pos, ..) = r.line_at(p, tol)?; + let (across, along) = if r.vertical { (p.x, p.y) } else { (p.y, p.x) }; + let (a, b) = (along - tol, along + tol); + ((pos - across).abs() <= tol) + .then(|| PathSeg::Line(if r.vertical { Line::new((pos, a), (pos, b)) } else { Line::new((a, pos), (b, pos)) })) + }; + let near: Vec = self + .segments + .iter() + .filter(|(b, c)| b.inflate(tol, tol).contains(p) && c.nearest(p, 1e-6).distance_sq <= tol * tol) + .map(|(_, c)| *c) + .chain(self.rulers.iter().filter_map(ruler)) + // shortcut: the first 32 lines in reach are paired, raise it if busier crossings matter + .take(32) + .collect(); + let mut best: Option = None; + for (i, a) in near.iter().enumerate() { + for b in near.iter().skip(i + 1) { + let hits: Vec = match (a, b) { + (PathSeg::Line(l), c) | (c, PathSeg::Line(l)) => c.intersect_line(*l).iter().map(|x| l.eval(x.line_t)).collect(), + _ => meeting(a, b, p).into_iter().collect(), + }; + for q in hits { + if q.distance(p) <= tol && best.is_none_or(|b| q.distance(p) < b.distance(p)) { + best = Some(q); + } + } + } + } + best + } + + /// Snap a ruler guide at `v` (the x of a vertical one, the y of a horizontal one) into line + /// with the nearest edge, centre or anchor within `tol`: where it goes, and a label there. + pub fn snap_guide(&self, vertical: bool, v: f64, tol: f64) -> (f64, Vec) { + let along = |q: &Point| if vertical { q.x } else { q.y }; + let lines = if vertical { &self.xs } else { &self.ys }; + let best = lines_near(lines, v, tol) + .iter() + .map(|(t, from, k)| (*t, *from, *k)) + .chain(self.points.iter().map(|(q, k)| (along(q), *q, *k))) + .filter(|(t, ..)| (t - v).abs() <= tol) + .min_by(|a, b| (a.0 - v).abs().total_cmp(&(b.0 - v).abs())); + match best { + Some((t, from, k)) => (t, self.label(from, k.label()).into_iter().collect()), + None => (v, vec![]), + } + } + + /// Snap a dragged point that carries others along at `offsets` (a resize handle and the + /// bleed edge beyond it): onto an anchor or centre near the point itself, otherwise into line + /// with targets, each axis on whichever of them comes nearest. + pub fn snap_point_with(&self, p: Point, offsets: &[Vec2], tol: f64) -> (Point, Vec) { + if self.nearest_point(p, tol).is_some() { + return self.snap_point(p, tol); + } + let rects: Vec = std::iter::once(Vec2::ZERO).chain(offsets.iter().copied()).map(|o| Rect::from_points(p + o, p + o)).collect(); + let (adj, mut ov, aligned) = self.align_rects(&rects, tol); + let out = p + adj; + if aligned.contains(&true) { + ov.extend(self.label(out, "align")); + } + (out, ov) + } + + /// Snap a moving rectangle (selection bounds after a move by `d`): tries its corners/edges/centre. + pub fn snap_rect(&self, r: Rect, tol: f64) -> (Vec2, Vec) { + self.snap_rects(&[r], tol) + } + + /// Snap a point that may only move along `dir` (the grabbed point of a move Shift keeps to its + /// line): onto the point of that line nearest an anchor or center within `tol` of `p`, labeled + /// at the anchor or center, else to where the nearest target line or ruler guide crosses the + /// line, at most twice `tol` away ([`Self::slide`]). Returns where it goes and the guides. + pub(crate) fn snap_point_along(&self, p: Point, dir: Vec2, tol: f64) -> (Point, Vec) { + let l2 = dir.hypot2(); + if !l2.is_finite() || l2 <= 1e-18 { + return (p, vec![]); + } + if let Some((q, k)) = self.nearest_point(p, tol) { + return (p + dir * ((q - p).dot(dir) / l2), self.label(q, k.label()).into_iter().collect()); + } + match self.slide(p, dir, p, tol, false) { + Some((q, from)) => (q, self.lined_up(q, from)), + None => (p, vec![]), + } + } + + /// [`Self::snap_rects`] for rectangles that may only move along `dir` (a move Shift keeps to + /// its line): they slide along it until the edge or center of one of them is in line with a + /// target, within `tol` of it on that axis, the target that needs the shortest way along the + /// line winning. Lining up with nothing, a single rectangle with Spacing Guides slides to where + /// it is spaced evenly along an axis the line runs along. Returns the shift (along `dir`) and + /// the guides. + pub(crate) fn snap_rects_along(&self, rects: &[Rect], dir: Vec2, tol: f64) -> (Vec2, Vec) { + let len = dir.hypot(); + if !len.is_finite() || len <= 1e-9 { + return (Vec2::ZERO, vec![]); + } + let u = dir / len; + // The axes the line runs along: the x axis (`true`) and the y axis, and how far a step of + // one along the line goes on that axis. + let axes = [(true, u.x), (false, u.y)].into_iter().filter(|(_, c)| c.abs() > 1e-9); + // The way along the line, the target line it lines up with, where that line starts, and + // its axis. + let mut best: Option<(f64, f64, Point, bool)> = None; + for (x, c) in axes.clone() { + let lines = if x { &self.xs } else { &self.ys }; + for r in rects { + let vs = if x { [r.x0, r.center().x, r.x1] } else { [r.y0, r.center().y, r.y1] }; + for v in vs { + for (t, from, _) in lines_near(lines, v, tol).iter().filter(|(t, ..)| (t - v).abs() <= tol) { + let s = (t - v) / c; + if best.is_none_or(|b| s.abs() < b.0.abs()) { + best = Some((s, *t, *from, x)); + } + } + } + } + } + if let Some((s, t, from, x)) = best { + let ov = rects.iter().map(|r| *r + u * s).reduce(|a, b| a.union(b)).and_then(|m| { + if x { + self.line(Point::new(t, from.y.min(m.y0)), Point::new(t, from.y.max(m.y1))) + } else { + self.line(Point::new(from.x.min(m.x0), t), Point::new(from.x.max(m.x1), t)) + } + }); + return (u * s, ov.into_iter().collect()); + } + let [r] = rects else { return (Vec2::ZERO, vec![]) }; + if self.boxes.is_empty() { + return (Vec2::ZERO, vec![]); + } + let spaced = + axes.filter_map(|(x, c)| self.even_spacing(*r, x, tol).map(|(shift, _)| (shift / c, x))).min_by(|a, b| a.0.abs().total_cmp(&b.0.abs())); + let Some((s, x)) = spaced else { return (Vec2::ZERO, vec![]) }; + let gaps = self.even_spacing(*r + u * s, x, tol).map(|(_, gaps)| gaps).unwrap_or_default(); + (u * s, gaps.into_iter().flat_map(|g| self.gap_marks(g, x, tol)).collect()) + } + + /// Snap rectangles that move together (an artboard and its bleed): the edge or centre of any + /// of them nearest a target, per axis. A single moved selection with Spacing Guides + /// ([`Self::for_move`]) is spaced evenly along an axis that lines up with nothing + /// ([`Self::even_spacing`]). Returns the shift and the guides. + pub fn snap_rects(&self, rects: &[Rect], tol: f64) -> (Vec2, Vec) { + let (mut d, mut ov, aligned) = self.align_rects(rects, tol); + if let [r] = rects + && !self.boxes.is_empty() + { + let moved = *r + d; + let mut spaced = false; + for (x, lined_up) in [true, false].into_iter().zip(aligned) { + if let Some((shift, _)) = self.even_spacing(moved, x, tol).filter(|_| !lined_up) { + *(if x { &mut d.x } else { &mut d.y }) += shift; + spaced = true; + } + } + // The equal gaps, marked where the rectangle ends up. + if spaced { + let done = *r + d; + for (x, lined_up) in [true, false].into_iter().zip(aligned) { + let gaps = if lined_up { None } else { self.even_spacing(done, x, tol) }; + ov.extend(gaps.into_iter().flat_map(|(_, gaps)| gaps).flat_map(|g| self.gap_marks(g, x, tol))); + } + } + } + (d, ov) + } + + /// Spacing Guides along the x axis (`x`) or the y axis for the moved rectangle `r`, among the + /// objects of its row (column): the shift (within `tol`) that leaves it as far from its + /// neighbour before or after it as two other objects of the row are apart, or as far from both + /// neighbours; and the equal gaps then, as (start, end, where across) along that axis. + fn even_spacing(&self, r: Rect, x: bool, tol: f64) -> Option<(f64, Vec<[f64; 3]>)> { + let (r0, r1) = span(&r, x); + let len = r1 - r0; + let (c0, c1) = span(&r, !x); + // The objects beside it, before or after, overlapping it across, by where they start. + let mut row: Vec<(Rect, f64, f64)> = self + .boxes + .iter() + .map(|b| (*b, span(b, x).0, span(b, x).1)) + .filter(|(b, a0, a1)| span(b, !x).0 < c1 && span(b, !x).1 > c0 && (*a1 <= r0 + tol || *a0 >= r1 - tol)) + .collect(); + row.sort_by(|a, b| a.1.total_cmp(&b.1)); + // Where two boxes overlap across: the middle of that (or of the space between them). + let mid = |a: &Rect, b: &Rect| (span(a, !x).0.max(span(b, !x).0) + span(a, !x).1.min(span(b, !x).1)) / 2.0; + // Each object and the nearest one after it, unless the moved rectangle sits between them. + let pairs: Vec<(f64, [f64; 3])> = row + .iter() + .filter_map(|(p, _, p1)| { + let next = row.partition_point(|(_, a0, _)| *a0 < *p1); + let (q, q0, _) = row.get(next)?; + let straddles = *p1 <= r0 + tol && *q0 >= r1 - tol; + (*q0 > *p1 && !straddles).then(|| (q0 - p1, [*p1, *q0, mid(p, q)])) + }) + .collect(); + let before = row.iter().filter(|(_, _, a1)| *a1 <= r0 + tol).max_by(|a, b| a.2.total_cmp(&b.2)); + let after = row.iter().filter(|(_, a0, _)| *a0 >= r1 - tol).min_by(|a, b| a.1.total_cmp(&b.1)); + // Where it could start, and the gap it repeats (None: halfway between its neighbours). + let mut starts: Vec<(f64, Option)> = vec![]; + if let (Some(a), Some(b)) = (before, after) + && b.1 - a.2 > len + { + starts.push(((a.2 + b.1 - len) / 2.0, None)); + } + for (g, _) in &pairs { + starts.extend(before.map(|a| (a.2 + g, Some(*g)))); + starts.extend(after.map(|b| (b.1 - g - len, Some(*g)))); + } + let (start, repeats) = + starts.into_iter().filter(|(t, _)| (t - r0).abs() <= tol).min_by(|a, b| (a.0 - r0).abs().total_cmp(&(b.0 - r0).abs()))?; + let at = r + if x { Vec2::new(start - r0, 0.0) } else { Vec2::new(0.0, start - r0) }; + let (end, mut gaps) = (start + len, vec![]); + if let Some((a, _, a1)) = before + && repeats.is_none_or(|g| (start - a1 - g).abs() <= 1e-9) + { + gaps.push([*a1, start, mid(a, &at)]); + } + if let Some((b, b0, _)) = after + && repeats.is_none_or(|g| (b0 - end - g).abs() <= 1e-9) + { + gaps.push([end, *b0, mid(b, &at)]); + } + if let Some(g) = repeats { + gaps.extend(pairs.iter().filter(|(pg, _)| (pg - g).abs() <= 1e-9).map(|(_, gap)| *gap)); + } + Some((start - r0, gaps)) + } + + /// A Spacing Guides mark: a line across the gap (start, end, where across) along the x axis + /// (`x`) or the y axis, with a tick `tick` long either side of it at each end. + fn gap_marks(&self, [u0, u1, c]: [f64; 3], x: bool, tick: f64) -> [Overlay; 3] { + let pt = |u: f64, c: f64| if x { Point::new(u, c) } else { Point::new(c, u) }; + let line = |a: Point, b: Point| Overlay::Line { a, b, color: self.style.color, dashed: false }; + [line(pt(u0, c), pt(u1, c)), line(pt(u0, c - tick), pt(u0, c + tick)), line(pt(u1, c - tick), pt(u1, c + tick))] + } + + /// [`Self::snap_rects`] before Spacing Guides, also saying whether each axis (x, y) lined up + /// (even with Alignment Guides off, or already in line, when there is no line or shift to + /// tell). + fn align_rects(&self, rects: &[Rect], tol: f64) -> (Vec2, Vec, [bool; 2]) { + let nearest = |targets: &[(f64, Point, Kind)], along: fn(&Rect) -> [f64; 3]| { + rects + .iter() + .flat_map(|r| along(r).into_iter().map(move |v| (v, *r))) + .flat_map(|(v, r)| lines_near(targets, v, tol).iter().map(move |(t, from, _)| (t - v, *from, v, r))) + .filter(|(d, ..)| d.abs() <= tol) + .min_by(|a, b| a.0.abs().total_cmp(&b.0.abs())) + }; + let best_x = nearest(&self.xs, |r| [r.x0, r.center().x, r.x1]); + let best_y = nearest(&self.ys, |r| [r.y0, r.center().y, r.y1]); + let mut d = Vec2::ZERO; + let mut ov = vec![]; + if let Some((dx, from, x, r)) = best_x { + d.x = dx; + let x = x + dx; + let (y0, y1) = (from.y.min(r.y0), from.y.max(r.y1)); + ov.extend(self.line(Point::new(x, y0), Point::new(x, y1))); + } + if let Some((dy, from, y, r)) = best_y { + d.y = dy; + let y = y + dy; + let (x0, x1) = (from.x.min(r.x0), from.x.max(r.x1)); + ov.extend(self.line(Point::new(x0, y), Point::new(x1, y))); + } + (d, ov, [best_x.is_some(), best_y.is_some()]) + } + + /// Snap a bounding-box resize. `a` is the scale [`crate::bbox::scale_for_drag`] gave for + /// dragging `handle` of the box `bx` (about the centre when `from_center`), in the box's own + /// frame, and so is the result. The handle lands on an anchor or centre within `tol` on the page + /// (labelled as [`Self::snap_point`] does), else the corners and the side it moves line up with + /// the nearest target or ruler guide ("align"), whatever the box's angle. A handle with one way + /// to go (a side, or a proportional corner) slides along it until one of them is on a target. + pub fn snap_scale(&self, bx: &OrientedBox, handle: Handle, a: Affine, proportional: bool, from_center: bool, tol: f64) -> (Affine, Vec) { + type Hit = Option<(f64, Point)>; + // Where a snap lands: in line with a target's x and/or y, or on an anchor or centre. + type Snap = (Hit, Hit, Option<(Point, Kind)>); + let r = bx.rect; + let origin = if from_center { r.center() } else { handle.opposite().pos(r) }; + let (d0, [sx, _, _, sy, _, _]) = (handle.pos(r) - origin, a.as_coeffs()); + let (to_doc, to_local) = (bx.to_doc(), bx.to_doc().inverse()); + // A scale that collapses or flips the box is no snap. + let valid = |n: f64, s: f64| n.is_finite() && n * s > 0.0; + // The target line along the x axis (`x`: a vertical line) or the y axis nearest `p`, the + // ruler guides running past it included. + let nearest = |p: Point, x: bool| -> Hit { + let (v, ts) = if x { (p.x, &self.xs) } else { (p.y, &self.ys) }; + let rulers = self.rulers.iter().filter(|r| r.vertical == x).filter_map(|r| r.line_at(p, tol)); + lines_near(ts, v, tol) + .iter() + .copied() + .chain(rulers) + .map(|(t, from, _)| (t, from)) + .filter(|(t, _)| (t - v).abs() <= tol) + .min_by(|p, q| (p.0 - v).abs().total_cmp(&(q.0 - v).abs())) + }; + let (mut nx, mut ny) = (sx, sy); + let mut snap: Snap = (None, None, None); + if handle.is_corner() && !proportional { + // The corner goes anywhere: onto an anchor or centre, else it takes the target's x, its + // y, or both. + let p = to_doc * (a * handle.pos(r)); + let (hx, hy) = (nearest(p, true), nearest(p, false)); + let on = self.nearest_point(p, tol).map(|(q, k)| (Some((q.x, q)), Some((q.y, q)), Some((q, k)))); + for (tx, ty, at) in on.into_iter().chain([(hx, hy, None), (hx, None, None), (None, hy, None)]) { + if tx.is_none() && ty.is_none() { + continue; + } + let q = to_local * Point::new(tx.map_or(p.x, |h| h.0), ty.map_or(p.y, |h| h.0)); + let (mut cx, mut cy) = ((q.x - origin.x) / d0.x, (q.y - origin.y) / d0.y); + // Square to the page, the axis without a target stays exactly where it was. + if bx.angle == 0.0 { + (cx, cy) = (if tx.is_some() { cx } else { sx }, if ty.is_some() { cy } else { sy }); + } + if valid(cx, sx) && valid(cy, sy) { + (nx, ny, snap) = (cx, cy, (tx, ty, at)); + break; + } + } + } else { + // One parameter `s`: the scale is `c + s·e` on each axis. + let (ax, _) = handle.axes(); + let (e, c, s0) = match (handle.is_corner(), ax, proportional) { + (true, _, _) => ((sx.signum(), sy.signum()), (0.0, 0.0), sx.abs()), + (false, true, true) => ((1.0, sx.signum()), (0.0, 0.0), sx), + (false, true, false) => ((1.0, 0.0), (0.0, 1.0), sx), + (false, false, true) => ((sy.signum(), 1.0), (0.0, 0.0), sy), + (false, false, false) => ((0.0, 1.0), (1.0, 0.0), sy), + }; + // The points that may land on a target: the handle, and the two ends of its side. + let i = handle as usize; + let ends = if handle.is_corner() { [None, None] } else { [Handle::ALL.get((i + 1) % 8), Handle::ALL.get((i + 7) % 8)] }; + // How far the handle travels per unit of `s`: a snap may not drag it far from the + // pointer, as it would for a target the side runs almost along. + let travel = (e.0 * d0.x).hypot(e.1 * d0.y); + // The snap so far: its `s`, whether it only lines up (an anchor or centre comes first), + // how far it moves the handle, and where it lands. + let mut best: Option<(f64, bool, f64, Snap)> = None; + let mut consider = |s: f64, in_line: bool, found: Snap| { + let moved = (s - s0).abs() * travel; + if valid(s, s0) && moved <= 2.0 * tol && best.as_ref().is_none_or(|k| (in_line, moved) < (k.1, k.2)) { + best = Some((s, in_line, moved, found)); + } + }; + for h in std::iter::once(handle).chain(ends.into_iter().flatten().copied()) { + let v = h.pos(r) - origin; + let base = to_doc * (origin + Vec2::new(c.0 * v.x, c.1 * v.y)); + let dir = (to_doc * Point::new(e.0 * v.x, e.1 * v.y)).to_vec2(); + let p = base + dir * s0; + // An anchor or centre beside its way: it stops at the nearest point of the way. + let l2 = dir.hypot2(); + if l2 > 1e-18 + && let Some((q, k)) = self.nearest_point(p, tol) + { + consider((q - base).dot(dir) / l2, false, (None, None, Some((q, k)))); + } + for (is_x, b, d) in [(true, base.x, dir.x), (false, base.y, dir.y)] { + if d.abs() <= 1e-9 { + continue; + } + let Some(hit) = nearest(p, is_x) else { continue }; + consider((hit.0 - b) / d, true, if is_x { (Some(hit), None, None) } else { (None, Some(hit), None) }); + } + } + if let Some((s, _, _, found)) = best { + (nx, ny, snap) = (c.0 + s * e.0, c.1 + s * e.1, found); + } + } + let mut out = Affine::translate(origin.to_vec2()) * Affine::scale_non_uniform(nx, ny) * Affine::translate(-origin.to_vec2()); + let (hit_x, hit_y, on) = snap; + if let Some((q, k)) = on { + return (out, self.label(q, k.label()).into_iter().collect()); + } + // The lines run from the target's own point across the resized box, on the page. + let mut nr = (to_doc * out).transform_rect_bbox(r); + let mut ov = vec![]; + if let Some((x, from)) = hit_x { + ov.extend(self.line(Point::new(x, from.y.min(nr.y0)), Point::new(x, from.y.max(nr.y1)))); + } + if let Some((y, from)) = hit_y { + ov.extend(self.line(Point::new(from.x.min(nr.x0), y), Point::new(from.x.max(nr.x1), y))); + } + if hit_x.is_some() || hit_y.is_some() { + ov.extend(self.label(to_doc * (out * handle.pos(r)), "align")); + } + if !self.boxes.is_empty() && !from_center { + let aligned = [hit_x.is_some(), hit_y.is_some()]; + for (is_x, lined_up) in [(true, aligned[0]), (false, aligned[1])] { + if lined_up { + continue; + } + let Some(moves_min) = resize_moves_min_edge(handle, is_x) else { continue }; + if let Some((shift, gaps)) = self.even_spacing(nr, is_x, tol) { + let (lo, hi) = span(&nr, is_x); + let len = hi - lo; + let new_len = if moves_min { hi - (lo + shift) } else { hi + shift - lo }; + if len > 1e-9 && new_len > 1e-9 { + let f = new_len / len; + let (ax, ay) = handle.axes(); + if is_x { + nx *= f; + if proportional && ay { + ny *= f; + } + } else { + ny *= f; + if proportional && ax { + nx *= f; + } + } + out = Affine::translate(origin.to_vec2()) * Affine::scale_non_uniform(nx, ny) * Affine::translate(-origin.to_vec2()); + nr = (to_doc * out).transform_rect_bbox(r); + ov.extend(gaps.into_iter().flat_map(|g| self.gap_marks(g, is_x, tol))); + } + } + } + } + (out, ov) + } +}