From aeafc359ea8a91e44776a4934825df144bdd5574 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 11:20:09 -0800 Subject: [PATCH 1/2] Measure tool: Shift keeps the end on the pointer's step after the grid rounds it With a Constrain Angle other than 0, the grid point a Shift-drag's end rounds to can lie nearer another 45-degree step, and the measurement then reported that step. A Shift-drag from (0, 0) to (5, -3) with a 30-degree Constrain Angle and a 9 pt grid measured -15 degrees instead of 30. The end is now turned back onto the step nearest the pointer's direction, keeping its length. --- crates/tools/src/xform/measure.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/tools/src/xform/measure.rs b/crates/tools/src/xform/measure.rs index 140697dd6..cf04c85c5 100644 --- a/crates/tools/src/xform/measure.rs +++ b/crates/tools/src/xform/measure.rs @@ -27,7 +27,8 @@ pub fn measure(a: Point, b: Point) -> (f64, f64, f64, f64) { /// it. With Shift held, the end stays on the 45° step from the Constrain Angle nearest the /// 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. +/// 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. pub(crate) fn end_point(cx: &ToolContext, a: Point, p: Point, shift: bool) -> Point { if !shift { return crate::guides::snap_draw(cx, p, &[]).0; @@ -42,7 +43,9 @@ pub(crate) fn end_point(cx: &ToolContext, a: Point, p: Point, shift: bool) -> Po let s = if to_point { crate::guides::snap_draw(cx, p, &[]).0 } else { p }; let q = a + u * (s - a).dot(u); let leave = crate::guides::Leave::segment(cx, a, true); - crate::guides::PointSnap::new(cx, || crate::guides::Targets::collect(cx.doc, &[], None)).snap_from(cx, q, Some(&leave)).0 + 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() } } impl Tool for MeasureTool { @@ -222,6 +225,27 @@ mod tests { assert!((o["dy"].as_f64().unwrap() + 100.0).abs() < 1e-9, "{o}"); } + /// With a Constrain Angle of 30°, the grid point a Shift-drag's end rounds to can lie nearer + /// another step; the end stays on the step nearest the pointer's direction. + #[test] + fn shift_drag_keeps_its_step_after_the_grid_rounds_it() { + 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_to_grid = true; + cx.constrain_angle = 30.0; + let shift = Mods { shift: true, ..Mods::default() }; + let mut t = MeasureTool::default(); + // (5, -3) points 31° up, so the step is 30°; the end lands at about (5.05, -2.92), which + // the 9 pt grid rounds to (9, 0), nearer the -15° step. + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 0.0, 0.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 5.0, -3.0).with_mods(shift)); + let o = t.options(); + assert!((o["angle"].as_f64().unwrap() - 30.0).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() { From 9051b700280896e7dfc9e2326db830025bf412f6 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 3 Nov 2026 11:34:00 -0800 Subject: [PATCH 2/2] 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() {