From 9051b700280896e7dfc9e2326db830025bf412f6 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 11:34:00 -0800 Subject: [PATCH] Measure tool: Shift does not turn the end to the opposite step With Snap to Point alone, an anchor behind the start could pull the projected end behind it, and the measurement read 180 degrees from the pointer's step. Such an end is now turned back onto the pointer's step, keeping its length, as an end rounded nearer another step is. --- crates/tools/src/xform/measure.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/tools/src/xform/measure.rs b/crates/tools/src/xform/measure.rs index cf04c85c5..62b207180 100644 --- a/crates/tools/src/xform/measure.rs +++ b/crates/tools/src/xform/measure.rs @@ -28,7 +28,8 @@ pub fn measure(a: Point, b: Point) -> (f64, f64, f64, f64) { /// pointer's direction: `p` is projected onto the step, then snapped along it as the Line Segment /// tool snaps a Shift-drawn end. With Snap to Point alone, the point `p` snaps to (an anchor, or /// in line with a ruler guide) is projected instead. When the snapped point lies nearer another -/// step (the grid rounds it past the halfway angle), it is turned back onto the pointer's step. +/// step (the grid rounds it past the halfway angle) or behind the start, it is turned back onto +/// the pointer's step. pub(crate) fn end_point(cx: &ToolContext, a: Point, p: Point, shift: bool) -> Point { if !shift { return crate::guides::snap_draw(cx, p, &[]).0; @@ -45,7 +46,7 @@ pub(crate) fn end_point(cx: &ToolContext, a: Point, p: Point, shift: bool) -> Po let leave = crate::guides::Leave::segment(cx, a, true); let r = crate::guides::PointSnap::new(cx, || crate::guides::Targets::collect(cx.doc, &[], None)).snap_from(cx, q, Some(&leave)).0; let v = r - a; - if v.cross(u).abs() <= 1e-9 * v.hypot().max(1.0) { r } else { a + u * v.hypot() } + if v.cross(u).abs() <= 1e-9 * v.hypot().max(1.0) && v.dot(u) >= 0.0 { r } else { a + u * v.hypot() } } impl Tool for MeasureTool { @@ -246,6 +247,30 @@ mod tests { assert!((o["angle"].as_f64().unwrap() - 30.0).abs() < 1e-9, "{o}"); } + /// With Snap to Point alone, an anchor behind the start can pull the projected end behind it; + /// the end is turned back onto the pointer's step instead of reading 180°. + #[test] + fn shift_drag_keeps_its_step_when_an_anchor_lies_behind_the_start() { + let (d, _) = doc_with_rect(); + let s = Selection::default(); + let p = paint(); + let mut cx = cx(&d, &s, &p); + cx.smart_guides = false; + cx.snap_tolerance = 4.0; + cx.snapping_tolerance = 4.0; + assert!(cx.snap_to_point); + let shift = Mods { shift: true, ..Mods::default() }; + let mut t = MeasureTool::default(); + // From (200.3, 196.005), the pointer at (202.3, 196.805) points 0° and snaps to the anchor + // at (200, 200), which projects onto the 0° line at (200, 196.005), behind the start. + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 200.3, 196.005)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 202.3, 196.805).with_mods(shift)); + let o = t.options(); + assert!(o["angle"].as_f64().unwrap().abs() < 1e-9, "{o}"); + assert!((o["dx"].as_f64().unwrap() - 0.3).abs() < 1e-9, "{o}"); + assert!(o["dy"].as_f64().unwrap().abs() < 1e-9, "{o}"); + } + /// With Snap to Point alone, a Shift-drag's end slides along its 45° step to a ruler guide. #[test] fn shift_drag_meets_a_ruler_guide() {