Fix #4312: a Shift move lands the selection's edges and centre on ruler guides

main lines a moved selection's edges and centre up with ruler guides
(snap_moved_to_rulers, #5746) but not when Shift keeps the move to its line. The
bounds now slide along that line onto a ruler guide the same way
(snap_moved_along_to_rulers), when nothing else lined up and the grabbed point is
not on a guide, within twice the Snap to Point distance along the line.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
SofiaAFigueredo
2026-11-11 09:51:18 -03:00
co-authored by Claude Sonnet 5.5
parent f3da0e9250
commit 337a2e02f5
5 changed files with 93 additions and 0 deletions
+41
View File
@@ -1194,6 +1194,47 @@ impl Targets {
(d, ov, held.contains(&true))
}
/// [`Self::snap_moved_to_rulers`] for a selection a move Shift keeps to its line (through the
/// grabbed point `p`, along `dir`), its bounds now `r`: the edge or centre of `r` that a ruler
/// guide runs past within `tol` goes onto it by sliding along the line, the way needing the
/// shortest slide winning and at most twice `tol` (a line nearly parallel to a guide would
/// carry the selection far to meet it). Returns the shift along the line and the guides.
pub(crate) fn snap_moved_along_to_rulers(&self, p: Point, r: Rect, dir: Vec2, tol: f64) -> (Vec2, Vec<Overlay>) {
let len = dir.hypot();
if !len.is_finite() || len <= 1e-9 {
return (Vec2::ZERO, vec![]);
}
let u = dir / len;
// The way along the line, where the guide is and where it starts, and whether it is upright.
let mut best: Option<(f64, f64, Point, bool)> = None;
for (vertical, c) in [(true, u.x), (false, u.y)] {
if c.abs() <= 1e-9 {
continue;
}
let vs = if vertical { [r.x0, r.center().x, r.x1] } else { [r.y0, r.center().y, r.y1] };
let (mid, half) =
if vertical { (Point::new(0.0, r.center().y), r.height() / 2.0) } else { (Point::new(r.center().x, 0.0), r.width() / 2.0) };
for (t, from, _) in self.rulers.iter().filter(|g| g.vertical == vertical).filter_map(|g| g.line_at(mid, tol + half)) {
for v in vs {
let s = (t - v) / c;
if (t - v).abs() <= tol && s.abs() <= 2.0 * tol && best.is_none_or(|b| s.abs() < b.0.abs()) {
best = Some((s, t, from, vertical));
}
}
}
}
let Some((s, t, from, vertical)) = best else { return (Vec2::ZERO, vec![]) };
let moved = r + u * s;
let line = if vertical {
self.line(Point::new(t, from.y.min(moved.y0)), Point::new(t, from.y.max(moved.y1)))
} else {
self.line(Point::new(from.x.min(moved.x0), t), Point::new(from.x.max(moved.x1), t))
};
let mut ov: Vec<Overlay> = line.into_iter().collect();
ov.extend(self.label(p + u * s, "align"));
(u * s, ov)
}
/// Snap a bounding-box resize. `a` is the scale [`crate::bbox::scale_for_drag`] gave for
/// dragging `handle` of the box `bx` (about the centre when `from_center`), in the box's own
/// frame, and so is the result. The handle lands on an anchor or centre within `tol` on the page
+19
View File
@@ -22,6 +22,25 @@ fn snaps_to_anchor_and_alignment() {
assert_eq!(p.y, 200.0);
}
/// #4312: a Shift move slides the selection along its line onto a ruler guide, but not far to
/// meet one a nearly parallel line only reaches a long way off.
#[test]
fn a_slide_along_a_line_lands_the_bounds_on_a_ruler_guide_within_reach() {
let t = Targets { rulers: vec![Ruler { vertical: true, pos: 300.0, span: None }], ..Targets::default() };
let r = Rect::new(248.0, 10.0, 298.0, 40.0);
let p = Point::new(270.0, 25.0);
// Level: the right edge is 2 short of the guide, which is the way along the line.
assert_eq!(t.snap_moved_along_to_rulers(p, r, Vec2::new(1.0, 0.0), 4.0).0, Vec2::new(2.0, 0.0));
// On the diagonal it is 2.8 along.
let (d, ov) = t.snap_moved_along_to_rulers(p, r, Vec2::new(1.0, 1.0), 4.0);
assert!((d - Vec2::new(2.0, 2.0)).hypot() < 1e-9 && !ov.is_empty(), "{d:?}");
// Nearly upright, it would take about 118 along the line to meet the guide.
assert_eq!(t.snap_moved_along_to_rulers(p, r, Vec2::new(0.017, 1.0), 4.0).0, Vec2::ZERO);
// A guide of an artboard needs the selection to run past it.
let bounded = Targets { rulers: vec![Ruler { vertical: true, pos: 300.0, span: Some((100.0, 200.0)) }], ..Targets::default() };
assert_eq!(bounded.snap_moved_along_to_rulers(p, r, Vec2::new(1.0, 0.0), 4.0).0, Vec2::ZERO);
}
#[test]
fn rect_snap_aligns_edges() {
let (d, id) = doc_with_rect();
+8
View File
@@ -368,6 +368,14 @@ impl MoveSnap {
if q != start + d {
d = q - start;
guides = ov;
} else if guides.is_empty()
&& let Some(b) = self.bounds
{
// Nothing else lined up: the bounds slide along the line onto a ruler guide, as
// they do without Shift (#4312).
let (adj, ov) = t.snap_moved_along_to_rulers(start + d, b + d, u, cx.tol(cx.snap_tolerance));
d += adj;
guides = ov;
}
}
if cx.snap_to_pixel
+24
View File
@@ -770,6 +770,30 @@ fn a_move_snaps_the_grabbed_point_to_a_ruler_guide_with_smart_guides_on() {
assert_eq!(dx(&ToolContext { snap_to_point: false, ..cx(&d, &s, &p) }, 172.0), 22.0);
}
/// #4312: a Shift move keeps to its line while its edges and centre land on a ruler guide, as a
/// move without Shift does (#5746).
#[test]
fn a_shift_move_lands_the_edges_on_a_ruler_guide() {
let (mut d, id) = doc_with_rect();
d.guides = vec![vectorcraft_doc::Guide::new(true, 333.0)];
let mut s = Selection::default();
s.add(id);
let p = paint();
let c = cx(&d, &s, &p);
// Level; the left edge (100) would end 2 short of the guide, the pointer far from it.
let (m, _) = shift_moved(&c, (150.0 + 231.0, 150.0));
assert!(moved_by(m, (233.0, 0.0)), "{m:?}");
// The right edge, 1 past the guide.
let (m, _) = shift_moved(&c, (150.0 + 134.0, 150.0));
assert!(moved_by(m, (133.0, 0.0)), "{m:?}");
// Out of reach, or with Snap to Point off, the move stays as dragged.
let (m, _) = shift_moved(&c, (150.0 + 221.0, 150.0));
assert!(moved_by(m, (221.0, 0.0)), "{m:?}");
let off = ToolContext { snap_to_point: false, ..cx(&d, &s, &p) };
let (m, _) = shift_moved(&off, (150.0 + 231.0, 150.0));
assert!(moved_by(m, (231.0, 0.0)), "{m:?}");
}
/// Alt pressed during a move makes a copy, which snaps to the original it leaves behind (as
/// gathered before the previews moved it); released, the move doesn't.
#[test]
+1
View File
@@ -2125,6 +2125,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s
| 2026-10-20 | minor | Type core: Hyphenation… and Justification… no longer listed as missing (done in #2310, #2755); evidence: a paragraph's Auto Leading drives the Character panel's Leading, the leading nudge and Fill with Placeholder Text; scores unchanged |
| 2026-10-22 | minor | Drawing tools: a Pen, Anchor Point or Direct Selection drag's preview no longer stalls (#834: the position where the pointer re-entered the window could be skipped, and the move in the release's frame was lost); score and hours unchanged (estimated) |
| 2026-10-23 | minor | Guides and snapping: a path moved by a press on its stroke or beside its edge (Selection, Group Selection, Direct Selection), and a straight segment Direct Selection drags, snap by the nearest point of the path, so the path lands exactly on a ruler guide with "align" instead of the pressed spot landing there and the path a few pixels beside it (#3701); `tests_prefs::a_move_held_by_a_paths_stroke_lands_the_path_on_a_ruler_guide`, `tests_prefs::a_straight_segment_dragged_by_direct_selection_lands_on_a_ruler_guide`. Score and hours unchanged. |
| 2026-11-11 | minor | Guides and snapping: a Shift move slides the selection along its line onto a ruler guide by its edges and centre, as a move without Shift does (#4312); a guide a nearly parallel line only reaches a long way off pulls nothing. Score and hours unchanged (estimated). |
| 2026-10-22 | minor | Live vector effects: the Transform effect has Transform Objects, Transform Patterns and Scale Strokes & Effects, its checkboxes visible (#885); the area row now lists only the options on a fill's or stroke's own Transform effect. Score and hours unchanged (estimated). |
| 2026-10-23 | minor | COLR colour fonts (v0 and v1) draw and export as vector art; evidence added; scores unchanged. |
| 2026-10-22 | minor | Libraries panel: the buttons beside the library picker show the items as icons (the default) or in a list, colours and graphics as rows with their names (a colour's hex value too), dragged and dropped by row; graphic thumbnails are shrunk to the size they are drawn at, so thin strokes stay whole. Tests: `panels::libraries::tests::the_list_view_shows_names_and_takes_drops_by_row`, `the_panel_draws_its_libraries`. Scores and hours unchanged. |