diff --git a/crates/engine/tests/it/dimensions.rs b/crates/engine/tests/it/dimensions.rs index 7c4872d56..65b2f423c 100644 --- a/crates/engine/tests/it/dimensions.rs +++ b/crates/engine/tests/it/dimensions.rs @@ -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(); diff --git a/crates/tools/src/dimension.rs b/crates/tools/src/dimension.rs index 4f4760471..d3c7bdf83 100644 --- a/crates/tools/src/dimension.rs +++ b/crates/tools/src/dimension.rs @@ -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)>, } /// `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 { (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) { - 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) { + 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 { 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, mods: Mods) -> Vec { 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 } } diff --git a/docs/dimensions.md b/docs/dimensions.md index f457d8dcc..b8ece996a 100644 --- a/docs/dimensions.md +++ b/docs/dimensions.md @@ -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