From 89a35ded91a4b1e9d10bc8f364c946aeaa934f28 Mon Sep 17 00:00:00 2001 From: chordpli Date: Tue, 20 Oct 2026 09:01:45 +0900 Subject: [PATCH] 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. --- crates/geom/src/crop_drag.rs | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/geom/src/crop_drag.rs b/crates/geom/src/crop_drag.rs index add22a136..c868e156e 100644 --- a/crates/geom/src/crop_drag.rs +++ b/crates/geom/src/crop_drag.rs @@ -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 { + // 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 { 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() {