From e47d5426805e130a13dfc2201262864bf67940ab Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 10 Oct 2026 11:46:21 +0700 Subject: [PATCH] Fix #886: Smart Guides keep working on a Shift-constrained anchor drag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dragging anchors with Direct Selection snapped to points, paths and Smart Guides, but with Shift held the move kept its 45° angle and snapped to nothing. The grabbed anchor now slides along the constrained line onto the anchors, paths and alignment lines that cross it, as the drawing tools' Shift does (counted from General › Constrain Angle). Co-Authored-By: Claude Opus 5.5 --- crates/tools/src/direct.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index cf17647c..9020413f 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -32,7 +32,7 @@ use vectorcraft_geom::{Anchor, PathData, Point, Rect}; use crate::bbox::move_delta; use crate::corners::{self, CornerDrag, over_widget}; -use crate::guides::{HandleSnap, PointSnap, Targets}; +use crate::guides::{HandleSnap, Leave, PointSnap, Targets}; use crate::meshedit::MeshEdit; use crate::pathtype::{self, BracketDrag, over_bracket}; use crate::rulerguide::GuideEdit; @@ -428,12 +428,12 @@ impl Tool for DirectSelectionTool { self.anchor_snap = Some(PointSnap::new(cx, || Targets::for_anchor_drag(cx.doc, cx.selection))); } let mut d = move_delta(start, p, ev.mods.shift); - // The grabbed anchor snaps and the others follow it; Shift keeps the angle instead. + // The grabbed anchor snaps and the others follow it. Shift keeps the move at its + // angle, and the anchor slides along it onto what Smart Guides find there (#886). self.guides.clear(); - if !ev.mods.shift - && let Some(snap) = &self.anchor_snap - { - let (q, guides) = snap.snap(cx, grab + d); + if let Some(snap) = &self.anchor_snap { + let shift = ev.mods.shift.then(|| Leave::segment(cx, grab, true)); + let (q, guides) = snap.snap_from(cx, grab + (p - start), shift.as_ref()); (d, self.guides) = (q - grab, guides); } out.push(Action::Preview("path.moveAnchors".into(), json!({"dx": d.x, "dy": d.y}))); @@ -1022,10 +1022,16 @@ mod tests { // In line with B's own bottom-left corner, which stays put. let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (303.0, 330.0), none); assert_eq!((v, labels), (json!({"dx": 0.0, "dy": 30.0}), vec!["align".to_string()])); - // Shift keeps the move at 45° steps, unsnapped. - let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (203.0, 199.0), Mods { shift: true, ..none }); - let s = move_delta(Point::new(300.0, 300.0), Point::new(203.0, 199.0), true); - assert_eq!((v, labels), (json!({"dx": s.x, "dy": s.y}), vec![])); + // Shift keeps the move at 45° steps and still snaps along that line (#886): on the + // diagonal from B's corner, onto A's bottom-right corner… + let shift = Mods { shift: true, ..none }; + let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (203.0, 199.0), shift); + let near = |v: &serde_json::Value, (x, y): (f64, f64)| (v["dx"].as_f64().unwrap() - x).hypot(v["dy"].as_f64().unwrap() - y) < 1e-9; + assert!(near(&v, (-100.0, -100.0)) && labels == ["anchor"], "{v} {labels:?}"); + // …and with nothing in reach, at the pointer's distance along its angle. + let (v, labels) = drag(&mut t, &c, (300.0, 300.0), (380.0, 302.0), shift); + let s = move_delta(Point::new(300.0, 300.0), Point::new(380.0, 302.0), true); + assert!(near(&v, (s.x, s.y)) && labels.is_empty(), "{v} {labels:?}"); // Two anchors: the one pressed on (B's top-right) lands on A's corner. let two = anchors_of(b, &[(0, 0), (0, 1)]); let c = cx(&d, &two, &p);