# Conflicts:
#	crates/ui-egui/src/panels/detail.rs
This commit is contained in:
Claude
2026-10-11 11:39:40 +00:00
3 changed files with 57 additions and 6 deletions
+21 -2
View File
@@ -71,8 +71,9 @@ impl Frame {
ow,
oh,
crop,
flip_h: apply_crop && s.crop.flip_h,
flip_v: apply_crop && s.crop.flip_v,
// a flip is not part of the crop: the crop tool's uncropped view shows it too
flip_h: s.crop.flip_h,
flip_v: s.crop.flip_v,
warp: (!warp.is_identity()).then_some(warp),
view: None,
};
@@ -387,4 +388,22 @@ mod tests {
assert!((out.get(0, 0)[0] - 3.0).abs() < 1e-5);
assert!((out.get(3, 0)[0] - 0.0).abs() < 1e-5);
}
/// The crop tool's uncropped frame keeps the flips: Flip H / Flip V show while cropping, and
/// screen points map back through the mirror (the crop box is drawn and dragged right).
#[test]
fn uncropped_frame_keeps_the_flips() {
let mut s = DevelopSettings::default();
s.crop.flip_h = true;
s.crop.flip_v = true;
s.crop.geometry.rect = Rect::new(0.1, 0.2, 0.6, 0.9);
let src = Rgb32f::from_fn(4, 2, |x, y| [x as f32, y as f32, 0.0]);
let f = Frame::new(4, 2, &s, false);
let out = f.sample(&src, 4, 2);
assert!((out.get(0, 0)[0] - 3.0).abs() < 1e-5 && (out.get(0, 0)[1] - 1.0).abs() < 1e-5, "{:?}", out.get(0, 0));
assert!((out.get(3, 1)[0] - 0.0).abs() < 1e-5 && (out.get(3, 1)[1] - 0.0).abs() < 1e-5, "{:?}", out.get(3, 1));
// the whole image, mirrored: output top-left is the image's bottom-right
let p = f.out_to_norm(400, 200).apply(Point::new(0.0, 0.0));
assert!((p.x - 1.0).abs() < 1e-9 && (p.y - 1.0).abs() < 1e-9, "{p:?}");
}
}
+35 -3
View File
@@ -1255,11 +1255,13 @@ fn crop_overlay(app: &mut LightcraftApp, ui: &mut egui::Ui, resp: &egui::Respons
let shade = Color32::from_black_alpha(150);
let mut mesh = egui::epaint::Mesh::default();
let outer = [img.left_top(), img.right_top(), img.right_bottom(), img.left_bottom()];
// the box's corners in screen order (a flipped photo shows its top-left corner elsewhere)
let on_screen = flipped_corners(&pts, frame.flip_h, frame.flip_v);
for i in 0..4 {
let a = outer[i];
let b = outer[(i + 1) % 4];
let c = pts[(i + 1) % 4];
let dd = pts[i];
let c = on_screen[(i + 1) % 4];
let dd = on_screen[i];
let base = mesh.vertices.len() as u32;
for v in [a, b, c, dd] {
mesh.vertices.push(egui::epaint::Vertex { pos: v, uv: Pos2::ZERO, color: shade });
@@ -1509,6 +1511,19 @@ fn to_straight(n: Point, angle: f64, frame: &Frame) -> Point {
Point::new(r.x / w, r.y / h)
}
/// A crop box's corners (top-left, top-right, bottom-right, bottom-left of the unflipped photo)
/// in the order they appear on screen when the photo is shown flipped.
fn flipped_corners(pts: &[Pos2], flip_h: bool, flip_v: bool) -> [Pos2; 4] {
let at = |i: usize| pts.get(i).copied().unwrap_or(Pos2::ZERO);
let order = match (flip_h, flip_v) {
(false, false) => [0, 1, 2, 3],
(true, false) => [1, 0, 3, 2],
(false, true) => [3, 2, 1, 0],
(true, true) => [2, 3, 0, 1],
};
order.map(at)
}
fn frame_crop_quad(d: &DevelopSettings, frame: &Frame) -> [Point; 4] {
let f = Frame { crop: d.crop.geometry, ..frame.clone() };
f.crop_quad_norm()
@@ -2237,7 +2252,8 @@ mod tests {
assert!(near(super::rotate_cursor_turn(&tilted, rot(pos2(200.0, 60.0)), false), 30f32.to_radians()));
}
use super::{film_label, texture_side};
use super::{film_label, flipped_corners, texture_side};
use egui::pos2;
/// Issue #652: a native host reports the GPU's texture limit in the first frame's input only.
/// The loupe must still know it in every later frame (it read the raw input, found nothing and
@@ -2266,6 +2282,22 @@ mod tests {
assert_eq!(film_label("DSC_0001"), "DSC_0001");
assert_eq!(film_label(""), "");
}
/// With the photo flipped, the crop box's corners reach the screen mirrored; the dimming
/// outside the box pairs each photo corner with the box corner on the same side of the screen
/// (it used to pair them in data order, so a flipped photo was dimmed inside the box).
#[test]
fn flipped_crop_box_corners_come_in_screen_order() {
let screen = [pos2(10.0, 10.0), pos2(90.0, 10.0), pos2(90.0, 50.0), pos2(10.0, 50.0)];
// data order (top-left, top-right, bottom-right, bottom-left) as each flip shows it
let h = [screen[1], screen[0], screen[3], screen[2]];
let v = [screen[3], screen[2], screen[1], screen[0]];
let hv = [screen[2], screen[3], screen[0], screen[1]];
assert_eq!(flipped_corners(&screen, false, false), screen);
assert_eq!(flipped_corners(&h, true, false), screen);
assert_eq!(flipped_corners(&v, false, true), screen);
assert_eq!(flipped_corners(&hv, true, true), screen);
}
}
#[cfg(test)]
+1 -1
View File
@@ -250,7 +250,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts).
| LR-CROP-AUTO | Auto straighten | P1 | ✅ | `cmd:crop.autoStraighten` | crop-angle leveling from detected horizon/plumb lines (consensus required) |
| LR-CROP-ANGLE | Angle slider | P0 | ✅ | `ctl:crop.angle`, `crates/ui-egui/src/panels/right.rs` | the Straighten slider; an exact angle can also be typed in the Angle field under it (see LR-CROP-STRAIGHTEN) |
| LR-CROP-ROTATE90 | Rotate 90° | P0 | ✅ | `cmd:photo.rotateLeft`, `cmd:photo.rotateRight` | |
| LR-CROP-FLIP | Flip | P0 | ✅ | `cmd:photo.flipHorizontal`, `cmd:photo.flipVertical` | |
| LR-CROP-FLIP | Flip | P0 | ✅ | `cmd:photo.flipHorizontal`, `cmd:photo.flipVertical`, `crates/pipeline/src/geometry.rs` | the crop tool's uncropped view shows the flips too, with the crop box and its dimming mirrored to match |
| LR-CROP-OVERLAY | Crop overlays | P1 | ✅ | `cmd:view.cropOverlay`, `cmd:view.cropOverlayOrientation`, `crates/ui-egui/src/panels/crop_overlay.rs` | thirds, grid, golden ratio, diagonal, triangle, golden spiral (mirrored with ⇧O while cropping); no aspect-ratio overlays |
| LR-CROP-ZOOM | Zoom while cropping | P1 | ✅ | `cmd:view.zoom100`, `cmd:view.zoomIn`, `crates/ui-egui/src/panels/detail.rs` | zoom levels apply with the crop tool open (verified: 100% shows the photo at native size with the crop frame) |
| LR-CROP-GENEXPAND | Generative expand | OOS | 🚫 | | |