LR-CROP-RECT: a crop a handle drag left at a tilted edge still moves (#2299)

A handle drag stops where is_within_image's 1e-6 tolerance ends, so a
corner can sit right at it. The move solve then let that corner go 1e-9
further, the result failed the check, and the across-then-down fallback
left the frame stuck. Solve first with every corner kept within half the
tolerance, and only then where it is.
This commit is contained in:
chordpli
2026-10-22 07:34:48 +09:00
parent f443bed56f
commit 89a35ded91
+28 -3
View File
@@ -208,16 +208,22 @@ pub fn drag_crop(start: CropGeometry, handle: CropHandle, grab: Point, pointer:
/// The translation (pixels) nearest `want` that keeps the frame `r` inside a `w × h` image turned by `angle` degrees.
fn nearest_move(r: Rect, angle: f64, w: f64, h: f64, want: Vec2) -> Option<Vec2> {
// first within half of `is_within_image`'s tolerance, so a frame a handle drag left at its edge still moves
solve_move(r, angle, (w, h), want, 5e-7).or_else(|| solve_move(r, angle, (w, h), want, f64::INFINITY))
}
/// [`nearest_move`] with each corner kept at most `pull` (a fraction of the size) past the image, or where it is.
fn solve_move(r: Rect, angle: f64, (w, h): (f64, f64), want: Vec2, pull: f64) -> Option<Vec2> {
let back = Affine::rotate_about(-angle.to_radians(), Point::new(w / 2.0, h / 2.0));
let (ex, ey) = (back.apply_vec(Vec2::new(1.0, 0.0)), back.apply_vec(Vec2::new(0.0, 1.0)));
let (nx, ny) = (Vec2::new(ex.x, ey.x), Vec2::new(ex.y, ey.y));
// never tighter than where the frame is, plus a slack so a zero-width strip survives rounding
// plus a slack so a zero-width strip survives rounding
let (sx, sy) = (1e-9 * w, 1e-9 * h);
let mut planes = Vec::with_capacity(16);
for c in r.corners() {
let q = back.apply(c);
for (n, b, s) in [(nx, w - q.x, sx), (-nx, q.x, sx), (ny, h - q.y, sy), (-ny, q.y, sy)] {
planes.push((n, b.max(0.0) + s));
for (n, b, s, size) in [(nx, w - q.x, sx, w), (-nx, q.x, sx, w), (ny, h - q.y, sy, h), (-ny, q.y, sy, h)] {
planes.push((n, b.max(0.0).min(b + pull * size) + s));
}
}
if planes.iter().all(|(n, b)| n.dot(want) <= *b) {
@@ -578,6 +584,25 @@ mod tests {
assert!(g.is_within_image(W, H));
}
// Scenario: a frame a handle drag enlarged to a tilted edge (review of #2299)
#[test]
fn given_a_frame_enlarged_to_a_tilted_edge_by_a_handle_when_moved_then_it_still_slides() {
let a = 2.0 / 3.0;
for angle in [3.0, 6.0, -10.0, 12.0] {
let fit = crate::crop_fit_angle(W, H, angle, Some(a));
let small = CropGeometry { rect: Rect::from_center(fit.rect.center(), fit.rect.width() * 0.6, fit.rect.height() * 0.6), angle };
let big = drag(small, BottomRight, p(small.rect.x1, small.rect.y1), p(1.5, 1.5), Some(a));
let c = big.rect.center();
let g = drag(big, Move, c, p(c.x - 0.5, c.y), Some(a));
assert!(g.rect.x0 < big.rect.x0 - 0.2, "{angle}: stuck at {:?}", g.rect);
assert!(
g.is_within_image(W, H) && near(g.rect.width(), big.rect.width()) && near(g.rect.height(), big.rect.height()),
"{angle}: {:?}",
g.rect
);
}
}
// Scenario: a smaller frame pushed sideways past a tilted edge slides along it (#2299)
#[test]
fn given_a_small_frame_pushed_past_a_tilted_edge_then_it_stops_on_the_edge_and_slides_along_it() {