Merge pull request #3240 from vangel76/shift-snap-axis-3229

Viewer: a Shift-constrained drag snaps only along its axis
This commit is contained in:
FredApps authored and GitHub committed 2026-11-14 17:25:59 +01:00
commit c3cdaecdc6
6 files changed
+201 -18

No files matched your search

+33
View File
@@ -1177,3 +1177,36 @@ fn polystar_undo_sanitizes_live_and_parked_property_selections_before_uid_reuse(
assert_eq!(layer(&s, lid).props.prop("transform/positionZ").unwrap(), &position_z, "the reused UID must not become an implicit expression target");
}
}
/// #3229: a drag Shift keeps to one axis snaps only that axis. A point a few pixels off the axis
/// lines the feature up with it (along x: its x) without taking it off the axis, as a line
/// through the point would; lines along the drag are left out, and the point reaches only `tol`
/// off the axis. Unconstrained (`snap`) the same point takes both coordinates, the feature leaving
/// the axis.
#[test]
fn a_shift_drag_snaps_only_along_its_axis() {
use crate::viewer::snap_along;
let src = SnapSource::Layer(LayerId(1));
let corner = SnapTarget::point([450.0, 150.0], PRI_LAYER, src);
let row = SnapTarget::hline(151.0, PRI_GUIDE, SnapSource::Guide);
let column = SnapTarget::vline(449.0, PRI_GUIDE, SnapSource::Guide);
let feature = [[448.0, 153.0]];
assert_eq!(snap(&feature, &[corner, row], 8.0).unwrap().at, [450.0, 150.0]);
// Along x: x onto the corner's, y kept (the nearer row does not pull it either).
let s = snap_along(&feature, &[corner, row], 8.0, true).unwrap();
assert_eq!(s.delta, [2.0, 0.0]);
assert_eq!(s.at, [450.0, 153.0]);
// The point, and the line from it to the feature (the feedback).
assert_eq!(s.hits, [corner, SnapTarget::vline(450.0, PRI_LAYER, src).within(Some([150.0, 153.0]))]);
// Along y: y onto the corner's, x kept (the column is left out).
let s = snap_along(&feature, &[corner, column], 8.0, false).unwrap();
assert_eq!(s.delta, [0.0, -3.0]);
assert_eq!(s.at, [448.0, 150.0]);
assert_eq!(s.hits, [corner, SnapTarget::hline(150.0, PRI_LAYER, src).within(Some([450.0, 448.0]))]);
// A line across the drag still snaps; a nearer one wins over the point.
let s = snap_along(&feature, &[corner, column], 8.0, true).unwrap();
assert_eq!(s.at, [449.0, 153.0]);
assert_eq!(s.hits, [column]);
// The point reaches only the tolerance off the axis.
assert_eq!(snap_along(&[[448.0, 170.0]], &[corner], 8.0, true), None);
}
+44
View File
@@ -189,6 +189,50 @@ pub fn layer_features(ctx: &EvalCtx, layer: &Layer) -> Vec<[f64; 2]> {
out
}
/// [`snap`] for a drag Shift keeps to one axis (`along_x`: it moves along x and y stays put, else
/// the other way round): only the free coordinate snaps, so the feature never leaves the axis
/// (#3229). A point target acts as a line through it across the drag (a vertical line at its x
/// for a drag along x) that reaches it only within `tol` on each axis (a square around the point),
/// so the feature lines up with a point near the axis, not with every point in the comp; lines
/// along the drag (horizontal ones for a drag along x) would only move the locked coordinate and
/// are left out. The nearest target wins, ties to the lower priority value. A hit on a point
/// reports the point (the feedback box) and the line from it to where the feature landed.
pub fn snap_along(sources: &[[f64; 2]], targets: &[SnapTarget], tol: f64, along_x: bool) -> Option<Snap> {
// (Key, source, target, and the free coordinate it snaps to.)
let mut best: Option<((f64, u8), [f64; 2], SnapTarget, f64)> = None;
for s in sources {
let (free, along) = if along_x { (s[0], s[1]) } else { (s[1], s[0]) };
for t in targets {
// The line the target snaps as: its coordinate and extent along it.
let (line, span) = match (t.kind, along_x) {
(SnapKind::Point, true) => (t.pos[0], Some([t.pos[1] - tol, t.pos[1] + tol])),
(SnapKind::Point, false) => (t.pos[1], Some([t.pos[0] - tol, t.pos[0] + tol])),
(SnapKind::VLine, true) => (t.pos[0], t.span),
(SnapKind::HLine, false) => (t.pos[1], t.span),
(SnapKind::VLine, false) | (SnapKind::HLine, true) => continue,
};
let d = (line - free).abs();
if d > tol || span.is_some_and(|[a, b]| along < a || along > b) {
continue;
}
let key = (d, t.priority);
if best.as_ref().is_none_or(|(k, _, _, _)| better(key, *k)) {
best = Some((key, *s, *t, line));
}
}
}
let (_, s, t, line) = best?;
let (delta, at) = if along_x { ([line - s[0], 0.0], [line, s[1]]) } else { ([0.0, line - s[1]], [s[0], line]) };
let mut hits = vec![t];
if t.kind == SnapKind::Point {
// The alignment, drawn from the point to the feature.
let (from, to) = if along_x { (t.pos[1], at[1]) } else { (t.pos[0], at[0]) };
let l = if along_x { SnapTarget::vline(line, t.priority, t.source) } else { SnapTarget::hline(line, t.priority, t.source) };
hits.push(l.within(Some([from, to])));
}
Some(Snap { delta, hits, at })
}
/// A layer's content box in comp space: corners (top left, top right, bottom right, bottom
/// left), edge midpoints (top, right, bottom, left) and centre.
struct BoxPoints {