mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Dimension tool: Linear picks show and snap to Smart Guides
Picking the two points of a Linear dimension snapped to anchors and to the line under the pointer, but showed no guides and ignored the art around it. The picks now go through the drawing tools' Smart Guides (DrawSnap), hovering too, so the guides show before each click: a point slides into line with other objects' anchors and centres, and the second onto the construction guides through the first. An anchor of the line under the pointer still wins (it links the dimension to that path), then a guide, then the closest point of the line (#2432). The guides go once the dimension is being placed. Test: linear_picks_show_and_snap_to_smart_guides (fails on the old tool, which showed no overlays while hovering). docs/dimensions.md updated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
381b788f56
commit
1a6bc267d5
@@ -452,6 +452,38 @@ fn both_points_snap_onto_anchors_with_a_visible_indicator() {
|
||||
assert_eq!(text(&s, id), "144.00 pt", "linked to both anchors");
|
||||
}
|
||||
|
||||
/// Smart Guides for the Linear picks: hovering away from any line, the first point slides into
|
||||
/// line with another object's corner and its guides show before the click; the second slides onto
|
||||
/// the construction guide through the first. The dimension measures between the snapped points.
|
||||
#[test]
|
||||
fn linear_picks_show_and_snap_to_smart_guides() {
|
||||
use vectorcraft_engine::tools::Overlay;
|
||||
let mut s = session();
|
||||
run(&mut s, "shape.rectangle", json!({"x":300,"y":200,"width":80,"height":60}));
|
||||
run(&mut s, "select.none", json!({}));
|
||||
s.select_tool("dimension", ViewInfo::default()).unwrap();
|
||||
let overlays = |s: &mut Session| s.overlays(ViewInfo::default());
|
||||
// In line with the rectangle's top edge, 2.5 pt off it: the pick lands on its level.
|
||||
ev(&mut s, PointerKind::Move, 250.0, 202.5);
|
||||
let o = overlays(&mut s);
|
||||
assert!(o.iter().any(|o| matches!(o, Overlay::Anchor { p, .. } if p.x == 250.0 && p.y == 200.0)), "the pick shows on the guide: {o:?}");
|
||||
assert!(o.iter().any(|o| matches!(o, Overlay::Line { .. } | Overlay::Label { .. })), "its guides show before the click: {o:?}");
|
||||
ev(&mut s, PointerKind::Down, 250.0, 202.5);
|
||||
ev(&mut s, PointerKind::Up, 250.0, 202.5);
|
||||
// Straight below the first point, 1.5 pt off: the rubber band ends on the vertical through it.
|
||||
ev(&mut s, PointerKind::Move, 251.5, 260.0);
|
||||
let end = overlays(&mut s).into_iter().find_map(|o| if let Overlay::Line { b, dashed: true, .. } = o { Some(b) } else { None }).unwrap();
|
||||
assert_eq!((end.x, end.y), (250.0, 260.0), "on the construction guide");
|
||||
ev(&mut s, PointerKind::Down, 251.5, 260.0);
|
||||
ev(&mut s, PointerKind::Up, 251.5, 260.0);
|
||||
ev(&mut s, PointerKind::Move, 220.0, 230.0);
|
||||
assert!(!overlays(&mut s).iter().any(|o| matches!(o, Overlay::Label { .. })), "placing: the guides are gone");
|
||||
ev(&mut s, PointerKind::Down, 220.0, 230.0);
|
||||
ev(&mut s, PointerKind::Up, 220.0, 230.0);
|
||||
let id = s.doc().unwrap().selection.objects[0].0;
|
||||
assert_eq!(text(&s, id), "60.00 pt");
|
||||
}
|
||||
|
||||
/// The page content streams of exported PDF `v`, inflated.
|
||||
fn pdf_content(v: &Value) -> String {
|
||||
let bytes = vectorcraft_format::base64_decode(v["dataBase64"].as_str().unwrap()).unwrap();
|
||||
|
||||
@@ -63,6 +63,10 @@ pub struct DimensionTool {
|
||||
/// Whether Shift was down at the last pointer event: the picked line shows constrained.
|
||||
shift: bool,
|
||||
began: bool,
|
||||
/// Smart Guides for the Linear picks, hovering too: the guides show before each click.
|
||||
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>)>,
|
||||
}
|
||||
|
||||
/// `at` as a Shift click picks it: the direction from `last` snapped to 45° steps (from the
|
||||
@@ -133,24 +137,27 @@ fn nearest_on(cx: &ToolContext, edge: &SegmentRef, p: Point) -> Option<Point> {
|
||||
(q.x.is_finite() && q.y.is_finite()).then_some(q)
|
||||
}
|
||||
|
||||
/// Where a Linear pick at `p` lands: on an anchor within reach (kept as a link to its path),
|
||||
/// else on the closest point of the picked line, else on the usual snap targets or `p` itself.
|
||||
fn snap_linear(cx: &ToolContext, p: Point) -> (Point, Option<PointRef>) {
|
||||
let (mut at, _) = crate::guides::snap_pick(cx, p);
|
||||
let mut reference = None;
|
||||
if let Some(edge) = pick(cx, p) {
|
||||
if let Some((anchor, point)) = anchor_near(cx, &edge, p) {
|
||||
at = point;
|
||||
reference = Some(PointRef { node: edge.node, subpath: edge.subpath, anchor });
|
||||
} else if let Some(point) = nearest_on(cx, &edge, p) {
|
||||
// Not on an anchor: snap to the closest point of the line (#2432).
|
||||
at = point;
|
||||
}
|
||||
}
|
||||
(at, reference)
|
||||
}
|
||||
|
||||
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.
|
||||
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)
|
||||
{
|
||||
self.snap.clear();
|
||||
return (point, Some(PointRef { node: edge.node, subpath: edge.subpath, anchor }));
|
||||
}
|
||||
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)
|
||||
}
|
||||
fn target(&self, cx: &ToolContext, p: Point) -> Option<Target> {
|
||||
let edge = pick(cx, p)?;
|
||||
match self.kind {
|
||||
@@ -240,12 +247,14 @@ impl DimensionTool {
|
||||
self.lines.clear();
|
||||
self.dragging = false;
|
||||
self.began = false;
|
||||
self.snap.clear();
|
||||
self.snapped = None;
|
||||
}
|
||||
/// A plain click: picks a point or a line, or measures a circle's radius.
|
||||
fn click(&mut self, cx: &ToolContext, p: Point, target: Option<Target>, mods: Mods) -> Vec<Action> {
|
||||
match self.kind {
|
||||
Kind::Linear => {
|
||||
let (mut at, mut reference) = snap_linear(cx, p);
|
||||
let (mut at, mut reference) = self.snap_linear(cx, p);
|
||||
if mods.shift
|
||||
&& let Some((last, _)) = self.points.last()
|
||||
{
|
||||
@@ -329,9 +338,16 @@ impl Tool for DimensionTool {
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
if self.pending.is_some() {
|
||||
// Placing: the picks are done, their guides go.
|
||||
self.snap.clear();
|
||||
self.snapped = None;
|
||||
return self.preview(cx, ev.pos);
|
||||
}
|
||||
self.hover = self.target(cx, ev.pos);
|
||||
self.snapped = (self.kind == Kind::Linear && self.lines.is_empty()).then(|| self.snap_linear(cx, ev.pos));
|
||||
if self.snapped.is_none() {
|
||||
self.snap.clear();
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
@@ -430,7 +446,7 @@ impl Tool for DimensionTool {
|
||||
// Linear: show where the next click lands (anchor ring, or a dot on a line), and draw the
|
||||
// rubber band to that point, so the user sees the snap before clicking (#2516).
|
||||
let linear_pick = self.kind == Kind::Linear && self.pending.is_none() && self.lines.is_empty();
|
||||
let snapped = linear_pick.then(|| snap_linear(cx, self.at));
|
||||
let snapped = self.snapped.clone().filter(|_| linear_pick);
|
||||
if let Some((q, reference)) = &snapped
|
||||
&& (reference.is_some() || q.distance(self.at) > 1e-9)
|
||||
{
|
||||
@@ -442,6 +458,10 @@ impl Tool for DimensionTool {
|
||||
let b = if self.shift { constrained(cx, *p, target) } else { target };
|
||||
out.push(Overlay::Line { a: *p, b, color, dashed: true });
|
||||
}
|
||||
// The Smart Guides of the point the next click picks (after the rubber band).
|
||||
if linear_pick {
|
||||
out.extend(self.snap.guides().iter().cloned());
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -5,7 +5,9 @@ The **Dimension Tool** is in the Modify section of the toolbar. Its strip offers
|
||||
|
||||
- **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.
|
||||
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
|
||||
straight lines that meet (or would, extended), then click to place.
|
||||
- **Radial:** hover a circle, or a segment that is a perfect arc, and drag: outwards gives its
|
||||
|
||||
Reference in New Issue
Block a user