mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 12:21:54 +00:00
Merge pull request #6409 from dotCooCoo/fix/dimension-anchor-clicks
Dimension tool: a click on an anchor of any path takes that anchor
This commit is contained in:
+124
-12
@@ -67,8 +67,13 @@ pub struct DimensionTool {
|
||||
snap: crate::guides::DrawSnap,
|
||||
/// Where the next Linear pick lands, as last worked out for the pointer (for the overlays).
|
||||
snapped: Option<(Point, Option<PointRef>)>,
|
||||
/// Whether the press under way began on an anchor a Linear click picks.
|
||||
press_on_anchor: bool,
|
||||
}
|
||||
|
||||
/// How near an anchor (screen pixels) a pick snaps to it.
|
||||
const ANCHOR_REACH: f64 = 8.0;
|
||||
|
||||
/// `at` as a Shift click picks it: the direction from `last` snapped to 45° steps (from the
|
||||
/// constrain angle), the distance kept.
|
||||
fn constrained(cx: &ToolContext, last: Point, at: Point) -> Point {
|
||||
@@ -119,10 +124,45 @@ fn anchor_near(cx: &ToolContext, edge: &SegmentRef, p: Point) -> Option<(usize,
|
||||
[edge.segment, next]
|
||||
.into_iter()
|
||||
.filter_map(|i| Some((i, sp.anchors.get(i)?.p)))
|
||||
.filter(|(_, a)| a.distance(p) <= cx.tol(8.0))
|
||||
.filter(|(_, a)| a.distance(p) <= cx.tol(ANCHOR_REACH))
|
||||
.min_by(|(_, a), (_, b)| a.distance(p).total_cmp(&b.distance(p)))
|
||||
}
|
||||
|
||||
/// The anchor nearest `p` within reach, on any editable path (the topmost on a tie), as a link to
|
||||
/// its path. Bounded work, and dimension artwork is skipped, as in [`pick`].
|
||||
fn anchor_under(cx: &ToolContext, p: Point) -> Option<(Point, PointRef)> {
|
||||
type Best = Option<(f64, Point, PointRef)>;
|
||||
fn visit(n: &Node, p: Point, reach: f64, budget: &mut usize, best: &mut Best) {
|
||||
if *budget == 0 || !n.visible || n.locked || n.extras().dimension.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Some(path) = n.path_data() {
|
||||
for (subpath, sp) in path.subpaths.iter().enumerate() {
|
||||
for (anchor, a) in sp.anchors.iter().enumerate() {
|
||||
if *budget == 0 {
|
||||
return;
|
||||
}
|
||||
*budget -= 1;
|
||||
let d = a.p.distance(p);
|
||||
if d <= reach && best.as_ref().is_none_or(|(nearest, ..)| d < *nearest) {
|
||||
*best = Some((d, a.p, PointRef { node: n.id, subpath, anchor }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(ch) = n.children() {
|
||||
for c in ch.iter().rev() {
|
||||
visit(c, p, reach, budget, best);
|
||||
}
|
||||
}
|
||||
}
|
||||
let (reach, mut budget, mut best) = (cx.tol(ANCHOR_REACH), 20_000usize, None);
|
||||
for n in cx.doc.layers.iter().rev() {
|
||||
visit(n, p, reach, &mut budget, &mut best);
|
||||
}
|
||||
best.map(|(_, at, r)| (at, r))
|
||||
}
|
||||
|
||||
/// The point of `edge` closest to `p`.
|
||||
fn nearest_on(cx: &ToolContext, edge: &SegmentRef, p: Point) -> Option<Point> {
|
||||
use vectorcraft_geom::kurbo::ParamCurveNearest;
|
||||
@@ -138,25 +178,22 @@ fn nearest_on(cx: &ToolContext, edge: &SegmentRef, p: Point) -> Option<Point> {
|
||||
}
|
||||
|
||||
impl DimensionTool {
|
||||
/// Where a Linear pick at `p` lands: on an anchor of the line under the pointer, within reach
|
||||
/// (kept as a link to its path); else where Smart Guides put it (another object's anchor or
|
||||
/// centre, in line with one, or for the second point on a construction guide through the
|
||||
/// first), its guides showing; else on the closest point of that line; else `p` itself.
|
||||
/// Where a Linear pick at `p` lands: on the nearest anchor of any path, within reach (kept as
|
||||
/// a link to its path); else where Smart Guides put it (another object's anchor or centre, in
|
||||
/// line with one, or for the second point on a construction guide through the first), its
|
||||
/// guides showing; else on the closest point of the line under the pointer; else `p` itself.
|
||||
fn snap_linear(&mut self, cx: &ToolContext, p: Point) -> (Point, Option<PointRef>) {
|
||||
let leave = self.points.last().map(|(first, _)| crate::guides::Leave::segment(cx, *first, false));
|
||||
let guided = self.snap.hover(cx, p, &[], leave.as_ref());
|
||||
let edge = pick(cx, p);
|
||||
if let Some(edge) = &edge
|
||||
&& let Some((anchor, point)) = anchor_near(cx, edge, p)
|
||||
{
|
||||
if let Some((point, reference)) = anchor_under(cx, p) {
|
||||
self.snap.clear();
|
||||
return (point, Some(PointRef { node: edge.node, subpath: edge.subpath, anchor }));
|
||||
return (point, Some(reference));
|
||||
}
|
||||
if guided.distance(p) > 1e-9 {
|
||||
return (guided, None);
|
||||
}
|
||||
// Not on an anchor or a guide: snap to the closest point of the line (#2432).
|
||||
(edge.and_then(|e| nearest_on(cx, &e, p)).unwrap_or(p), None)
|
||||
(pick(cx, p).and_then(|e| nearest_on(cx, &e, p)).unwrap_or(p), None)
|
||||
}
|
||||
fn target(&self, cx: &ToolContext, p: Point) -> Option<Target> {
|
||||
let edge = pick(cx, p)?;
|
||||
@@ -243,6 +280,7 @@ impl DimensionTool {
|
||||
fn reset(&mut self) {
|
||||
self.pending = None;
|
||||
self.press = None;
|
||||
self.press_on_anchor = false;
|
||||
self.points.clear();
|
||||
self.lines.clear();
|
||||
self.dragging = false;
|
||||
@@ -355,6 +393,10 @@ impl Tool for DimensionTool {
|
||||
self.press = Some((ev.pos, None));
|
||||
return self.preview(cx, ev.pos);
|
||||
}
|
||||
// A Linear press on an anchor picks the anchor unless it drags past the anchor's
|
||||
// reach: moving a few pixels before the release does not drag out the dimension of
|
||||
// a segment under the pointer.
|
||||
self.press_on_anchor = self.kind == Kind::Linear && anchor_under(cx, ev.pos).is_some();
|
||||
self.press = Some((ev.pos, self.target(cx, ev.pos)));
|
||||
vec![]
|
||||
}
|
||||
@@ -364,7 +406,7 @@ impl Tool for DimensionTool {
|
||||
}
|
||||
if self.pending.is_none()
|
||||
&& let Some((start, Some(target))) = self.press.clone()
|
||||
&& start.distance(ev.pos) > cx.tol(3.0)
|
||||
&& start.distance(ev.pos) > cx.tol(if self.press_on_anchor { ANCHOR_REACH } else { 3.0 })
|
||||
&& self.points.is_empty()
|
||||
&& self.lines.is_empty()
|
||||
{
|
||||
@@ -543,6 +585,76 @@ mod tests {
|
||||
assert!(matches!(placed.last(), Some(Action::Commit)), "{placed:?}");
|
||||
}
|
||||
|
||||
/// A Linear press on an anchor picks that anchor even when the pointer moves a few pixels
|
||||
/// before the release: it does not drag out the dimension of a segment that ends there. Both
|
||||
/// picks keep their links to the path, so the dimension measures between the two anchors.
|
||||
#[test]
|
||||
fn a_press_on_an_anchor_picks_it_when_the_pointer_moves() {
|
||||
let (d, id) = doc_with_rect();
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
let c = cx(&d, &s, &p);
|
||||
let ev = |kind, x: f64, y: f64| PointerEvent::new(kind, x, y);
|
||||
let mut t = DimensionTool::default();
|
||||
for ((x, y), (dx, dy)) in [((100.0, 100.0), (3.5, 1.0)), ((200.0, 200.0), (-4.0, -2.0))] {
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Down, x, y)).is_empty());
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Drag, x + dx, y + dy)).is_empty(), "no segment dimension is dragged out");
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Up, x + dx, y + dy)).is_empty(), "nothing is committed");
|
||||
}
|
||||
let d = t.pending.as_ref().expect("both anchors were picked");
|
||||
assert_eq!((d.a, d.b), (Point::new(100.0, 100.0), Point::new(200.0, 200.0)));
|
||||
let anchor = |anchor| Some(PointRef { node: id, subpath: 0, anchor });
|
||||
assert_eq!(d.source, Some(DimensionSource::Points { a: anchor(0), b: anchor(2), vertex: None }));
|
||||
// Dragged past the anchor's reach, the press drags out the dimension of a segment there.
|
||||
let mut t = DimensionTool::default();
|
||||
t.pointer(&c, &ev(PointerKind::Down, 100.0, 100.0));
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Drag, 103.0, 106.0)).is_empty());
|
||||
let acts = t.pointer(&c, &ev(PointerKind::Drag, 100.0, 60.0));
|
||||
assert!(matches!(acts.first(), Some(Action::Begin(_))), "{acts:?}");
|
||||
assert!(matches!(t.pointer(&c, &ev(PointerKind::Up, 100.0, 60.0)).last(), Some(Action::Commit)));
|
||||
}
|
||||
|
||||
/// The anchors of a live rectangle are picked the same way.
|
||||
#[test]
|
||||
fn a_press_on_a_live_shapes_anchor_picks_it() {
|
||||
let (d, id) = doc_with_live_rect();
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = DimensionTool::default();
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 200.0, 100.0));
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 197.0, 102.0));
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 197.0, 102.0));
|
||||
let [(at, Some(r))] = t.points.as_slice() else { panic!("{:?}", t.points) };
|
||||
assert_eq!((*at, r.node), (Point::new(200.0, 100.0), id));
|
||||
}
|
||||
|
||||
/// An anchor of a compound path inside nested groups, lying just inside another object's
|
||||
/// edge, is picked and linked, though that edge is nearer the pointer than any of the
|
||||
/// compound path's own segments. A press there that moves a few pixels picks it too.
|
||||
#[test]
|
||||
fn an_anchor_under_another_objects_edge_is_picked() {
|
||||
use std::sync::Arc;
|
||||
use vectorcraft_doc::{Appearance, NodeKind};
|
||||
use vectorcraft_geom::Rect;
|
||||
let (mut d, _) = doc_with_rect();
|
||||
let l = d.layers[0].id;
|
||||
let (outer, inner, compound) = (d.alloc_id(), d.alloc_id(), d.alloc_id());
|
||||
let mut path = vectorcraft_geom::shapes::rectangle(Rect::new(20.0, 20.0, 40.0, 40.0));
|
||||
path.subpaths.extend(vectorcraft_geom::shapes::rectangle(Rect::new(150.0, 103.0, 180.0, 130.0)).subpaths);
|
||||
let art = Node::path(compound, path, Appearance::default_art());
|
||||
let inner_group = Node::new(inner, NodeKind::Group { children: vec![Arc::new(art)], clip: false });
|
||||
let group = Node::new(outer, NodeKind::Group { children: vec![Arc::new(inner_group)], clip: false });
|
||||
d.insert(Some(l), 0, group).unwrap();
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
let c = cx(&d, &s, &p);
|
||||
let corner = (Point::new(150.0, 103.0), Some(PointRef { node: compound, subpath: 1, anchor: 0 }));
|
||||
let mut t = DimensionTool::default();
|
||||
assert_eq!(t.snap_linear(&c, Point::new(151.0, 101.0)), corner);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 151.0, 101.0));
|
||||
assert!(t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 155.0, 100.0)).is_empty(), "no segment dimension is dragged out");
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 155.0, 100.0));
|
||||
assert_eq!(t.points, vec![corner]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placement_press_previews_without_a_prior_move() {
|
||||
let (d, _) = doc_with_rect();
|
||||
|
||||
+5
-2
@@ -4,8 +4,11 @@ The **Dimension Tool** is in the Modify section of the toolbar. Its strip offers
|
||||
**Angular**, **Radial** and **Tool Options…** (double-clicking the tool opens Tool Options too).
|
||||
|
||||
- **Linear:** hover a path segment and drag outwards: releasing places the dimension. Or click two
|
||||
points (anchors snap, Shift constrains to 45°), then click to place. A curved segment is measured
|
||||
from end to end. With Smart Guides on, both points also snap into line with other objects'
|
||||
points (anchors snap, Shift constrains to 45°), then click to place. A click within 8 pixels of
|
||||
an anchor of any path, in groups and compound paths too, takes that anchor, and the dimension
|
||||
stays linked to it. A press on an anchor picks it when the pointer moves less than 8 pixels
|
||||
before the release; a longer drag drags out the dimension of the segment under the pointer. A
|
||||
curved segment is measured from end to end. With Smart Guides on, both points also snap into line with other objects'
|
||||
anchors and centres, and the second onto the construction guides through the first; the guides
|
||||
show while hovering, before each click.
|
||||
- **Angular:** hover a corner where two straight segments meet and drag outwards. Or click two
|
||||
|
||||
Reference in New Issue
Block a user