Fix #886: Smart Guides keep working on a Shift-constrained anchor drag

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 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-10 11:49:56 +07:00
co-authored by Claude Opus 5.5
parent d8759492df
commit e47d542680
+16 -10
View File
@@ -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);