Keep object snapping when Snap to Pixel Grid is on

Fixes #2029

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilverandClaude Opus 5.5 committed 2026-11-06 04:10:50 +01:00
1 parent 8eb0b87646
commit 39c5833818
3 files changed
+47 -15

No files matched your search

+14 -12
View File
@@ -218,15 +218,16 @@ impl Session {
// Cmd/Ctrl (and Control on macOS) held while dragging suspends snapping.
let snap_on = self.ds().map(|ds| ds.view.snap_objects).unwrap_or(true) && !ev.cmd && !ev.ctrl;
let mut guides = Vec::new();
if self.ds().map(|ds| ds.view.snap_pixel).unwrap_or(true) {
// Keep the box's origin on whole pixels (a rotated parent frame's, inside one);
// snapping to layers comes after, so it wins on the axes it snaps.
d = targets.round_move(d);
}
if snap_on {
let (c, g2) = targets.snap_move(d, SNAP_PX / ev.zoom.max(1e-6));
d += c;
guides = g2;
}
if self.ds().map(|ds| ds.view.snap_pixel).unwrap_or(true) {
// Keep the box's origin on whole pixels (a rotated parent frame's, inside one).
d = targets.round_move(d);
}
let ids = ids.clone();
let dup = *duplicate;
let page = self.page()?;
@@ -475,20 +476,21 @@ impl Session {
let targets = if snap { Some(Targets::gather(self.doc()?, page, &ids)) } else { None };
let mut p = ev.p();
let mut guides = Vec::new();
let _ = start;
// In the handle box's own axes (one layer: its local space).
let (frame, w0, h0) = r.frame;
// The pixel grid: an upright box's dragged sides land on whole pixels (the Scale tool
// scales by the factor as dragged). Snapping to layers comes after, so it wins on the axes
// it snaps.
if !scale_tool && self.ds().map(|d| d.view.snap_pixel).unwrap_or(true) && crate::guides::axis_aligned(frame) {
p = Point::new(p.x.round(), p.y.round());
}
if let Some(t) = &targets {
let (dx, dy, g) = t.snap_point(p.x, p.y, SNAP_PX / ev.zoom.max(1e-6));
p.x += dx;
p.y += dy;
guides = g;
}
let _ = start;
// In the handle box's own axes (one layer: its local space).
let (frame, w0, h0) = r.frame;
// The pixel grid: an upright box's dragged sides land on whole pixels (the Scale tool
// scales by the factor as dragged).
if !scale_tool && self.ds().map(|d| d.view.snap_pixel).unwrap_or(true) && crate::guides::axis_aligned(frame) {
p = Point::new(p.x.round(), p.y.round());
}
let q = frame.inverse() * p;
let is_line = r.single && ids.first().and_then(|i| self.doc().ok()?.node(*i).map(|n| matches!(n.kind, NodeKind::Line))).unwrap_or(false);
if scale_tool {
+6 -3
View File
@@ -29,20 +29,23 @@ impl Session {
let tool = self.tool.tool;
let mut p = ev.p();
let mut guides = Vec::new();
let pixel = self.ds().map(|d| d.view.snap_pixel).unwrap_or(true);
// The pixel grid first: snapping to layers comes after, so it wins on the axes it snaps.
if pixel {
p = Point::new(p.x.round(), p.y.round());
}
if self.ds().map(|d| d.view.snap_objects).unwrap_or(true) && !ev.cmd && !ev.ctrl {
let (dx, dy, g) = targets.snap_point(p.x, p.y, SNAP_PX / ev.zoom.max(1e-6));
p.x += dx;
p.y += dy;
guides = g;
}
let pixel = self.ds().map(|d| d.view.snap_pixel).unwrap_or(true);
let mut created = id;
let page = self.page()?;
self.preview(|doc, sel| {
let (mut a, mut b) = (start, p);
let (mut a, b) = (start, p);
if pixel {
a = Point::new(a.x.round(), a.y.round());
b = Point::new(b.x.round(), b.y.round());
}
let pw = doc.world_transform(parent);
let (kind, base) = match tool {
@@ -625,6 +625,33 @@ fn smart_guides_snap_to_edges_centres_and_equal_spacing() {
assert_eq!(bounds(&s, b).origin(), Point::new(198.0, 2.0));
}
#[test]
fn snapping_to_layers_wins_over_the_pixel_grid() {
// Both on (the defaults): a layer's centre at x = 5.5 is still a target.
let mut s = Session::new();
let a = rect(&mut s, 0.0, 0.0, 11.0, 11.0, None);
let b = rect(&mut s, 200.0, 300.0, 100.0, 100.0, None);
let _ = a;
ex(&mut s, "select.set", json!({"ids": [b]}));
// Moving: B's centre (grabbed) snaps to A's; the free axis stays on whole pixels.
s.pointer(&ev(PointerKind::Down, Point::new(250.0, 350.0), NONE)).unwrap();
s.pointer(&ev(PointerKind::Move, Point::new(6.0, 352.4), NONE)).unwrap();
assert!(s.tool.overlay.guides.iter().any(|g| near(g.x0, 5.5) && near(g.x1, 5.5)), "{:?}", s.tool.overlay.guides);
s.pointer(&ev(PointerKind::Up, Point::new(6.0, 352.4), NONE)).unwrap();
assert_eq!(bounds(&s, b).origin(), Point::new(-44.5, 302.0), "moved onto A's centre");
ex(&mut s, "edit.undo", json!({}));
// Out of reach of any layer, a move lands on whole pixels.
drag(&mut s, Point::new(250.0, 350.0), Point::new(260.3, 360.6), NONE);
assert_eq!(bounds(&s, b).origin(), Point::new(210.0, 311.0));
ex(&mut s, "edit.undo", json!({}));
// Resizing: the left handle snaps to A's centre; the bottom one lands on whole pixels.
drag(&mut s, Point::new(200.0, 350.0), Point::new(6.0, 350.0), NONE);
assert!(near(bounds(&s, b).x0, 5.5), "{:?}", bounds(&s, b));
ex(&mut s, "edit.undo", json!({}));
drag(&mut s, Point::new(250.0, 400.0), Point::new(250.0, 450.6), NONE);
assert_eq!(size(&s, b), (100.0, 151.0));
}
#[test]
fn corner_cursors_stay_diagonal_whatever_the_proportions() {
let mut s = session();