diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 234476167..93a848942 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -642,19 +642,22 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } app.loupe_shown = Some((id, shown)); // a wipe shows the Before whole-frame render on its side of the line… + let line = crate::region::wipe_line(img_rect, canvas); if app.ui.before_after == BeforeAfter::Split && let Some(tex) = app.renderer.textures.get(&Slot::Before).filter(|t| t.photo == id) { let before_rect = fit_texture_rect(img_rect, tex.size); - let left = Rect::from_min_max(before_rect.min, pos2(before_rect.center().x, before_rect.bottom())); - p.image(tex.tex.id(), left, Rect::from_min_max(pos2(0.0, 0.0), pos2(0.5, 1.0)), Color32::WHITE); + let u = ((line.x - before_rect.left()) / before_rect.width().max(1.0)).clamp(0.0, 1.0); + let left = Rect::from_min_max(before_rect.min, pos2(before_rect.left() + u * before_rect.width(), before_rect.bottom())); + p.image(tex.tex.id(), left, Rect::from_min_max(pos2(0.0, 0.0), pos2(u, 1.0)), Color32::WHITE); } if app.ui.before_after == BeforeAfter::SplitTopBottom && let Some(tex) = app.renderer.textures.get(&Slot::Before).filter(|t| t.photo == id) { let before_rect = fit_texture_rect(img_rect, tex.size); - let top = Rect::from_min_max(before_rect.min, pos2(before_rect.right(), before_rect.center().y)); - p.image(tex.tex.id(), top, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 0.5)), Color32::WHITE); + let v = ((line.y - before_rect.top()) / before_rect.height().max(1.0)).clamp(0.0, 1.0); + let top = Rect::from_min_max(before_rect.min, pos2(before_rect.right(), before_rect.top() + v * before_rect.height())); + p.image(tex.tex.id(), top, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, v)), Color32::WHITE); } // …and the window renders go over the whole-frame ones, all of them, where they belong in the // frame (clipped to their pane, or their side of the line) @@ -666,12 +669,10 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { } // the line of a wipe over both if app.ui.before_after == BeforeAfter::Split { - let mid = img_rect.center().x; - p.line_segment([pos2(mid, img_rect.top()), pos2(mid, img_rect.bottom())], Stroke::new(1.5, Color32::WHITE)); + p.line_segment([pos2(line.x, img_rect.top()), pos2(line.x, img_rect.bottom())], Stroke::new(1.5, Color32::WHITE)); } if app.ui.before_after == BeforeAfter::SplitTopBottom { - let mid = img_rect.center().y; - p.line_segment([pos2(img_rect.left(), mid), pos2(img_rect.right(), mid)], Stroke::new(1.5, Color32::WHITE)); + p.line_segment([pos2(img_rect.left(), line.y), pos2(img_rect.right(), line.y)], Stroke::new(1.5, Color32::WHITE)); } if app.ui.show_clipping && !show_before && !fullscreen { clipping_overlay(app, &p, img_rect); diff --git a/crates/ui-egui/src/region.rs b/crates/ui-egui/src/region.rs index 8fe29e135..82b33b8f6 100644 --- a/crates/ui-egui/src/region.rs +++ b/crates/ui-egui/src/region.rs @@ -157,17 +157,26 @@ impl SizeHold { } } +/// Where the line of a wipe crosses: the middle of the part of the photo that is on screen. At +/// Fit that is the photo's middle; zoomed in, the photo's own middle is mostly off screen, and a +/// line there left the whole view to one side (#1351). +pub fn wipe_line(img: egui::Rect, canvas: egui::Rect) -> egui::Pos2 { + let shown = img.intersect(canvas); + if shown.is_positive() { shown.center() } else { img.center() } +} + /// Where a window of one side of a Before/After view may be drawn: its own pane in side by side /// and stacked views (a window carries margin beyond what its pane shows, which would otherwise /// spill into the neighbour), its side of the line in a wipe, else the canvas. pub fn window_clip(mode: crate::state::BeforeAfter, before_side: bool, pane: egui::Rect, canvas: egui::Rect, img: egui::Rect) -> egui::Rect { use crate::state::BeforeAfter; let (min, max) = (canvas.min, canvas.max); + let line = wipe_line(img, canvas); let clip = match (mode, before_side) { - (BeforeAfter::Split, true) => egui::Rect::from_min_max(min, egui::pos2(img.center().x, max.y)), - (BeforeAfter::SplitTopBottom, true) => egui::Rect::from_min_max(min, egui::pos2(max.x, img.center().y)), - (BeforeAfter::Split, false) => egui::Rect::from_min_max(egui::pos2(img.center().x, min.y), max), - (BeforeAfter::SplitTopBottom, false) => egui::Rect::from_min_max(egui::pos2(min.x, img.center().y), max), + (BeforeAfter::Split, true) => egui::Rect::from_min_max(min, egui::pos2(line.x, max.y)), + (BeforeAfter::SplitTopBottom, true) => egui::Rect::from_min_max(min, egui::pos2(max.x, line.y)), + (BeforeAfter::Split, false) => egui::Rect::from_min_max(egui::pos2(line.x, min.y), max), + (BeforeAfter::SplitTopBottom, false) => egui::Rect::from_min_max(egui::pos2(min.x, line.y), max), (BeforeAfter::SideBySide | BeforeAfter::TopBottom, _) => pane, _ => canvas, }; @@ -423,6 +432,10 @@ mod tests { assert_eq!((before.max.x, after.min.x), (500.0, 500.0), "the line is the image's middle"); assert_eq!(window_clip(SplitTopBottom, true, canvas, canvas, img).max.y, 300.0); assert_eq!(window_clip(SplitTopBottom, false, canvas, canvas, img).min.y, 300.0); + // zoomed in, the photo's middle (x = 1600) is off screen: the line is the view's middle + let zoomed = egui::Rect::from_min_max(egui::pos2(-200.0, -900.0), egui::pos2(3400.0, 1500.0)); + assert_eq!(window_clip(Split, false, canvas, canvas, zoomed).min.x, 500.0); + assert_eq!(window_clip(SplitTopBottom, true, canvas, canvas, zoomed).max.y, 300.0); assert_eq!(window_clip(Off, false, canvas, canvas, img), canvas); assert_eq!(window_clip(Original, true, canvas, canvas, img), canvas); } diff --git a/crates/ui-egui/src/tests_before_after.rs b/crates/ui-egui/src/tests_before_after.rs index 96fab9cbe..ae35a35b2 100644 --- a/crates/ui-egui/src/tests_before_after.rs +++ b/crates/ui-egui/src/tests_before_after.rs @@ -69,3 +69,66 @@ fn wb_selector_picks_the_same_point_in_the_before_view() { } } } + +/// Mean (r, g, b) of the 41 × 41 screen pixels around `at`. +fn mean_around(shot: &egui::ColorImage, at: egui::Pos2) -> [f32; 3] { + let mut sum = [0.0f32; 3]; + let mut n = 0.0; + for y in (at.y as usize - 20)..=(at.y as usize + 20) { + for x in (at.x as usize - 20)..=(at.x as usize + 20) { + let c = shot[(x, y)]; + sum[0] += f32::from(c.r()); + sum[1] += f32::from(c.g()); + sum[2] += f32::from(c.b()); + n += 1.0; + } + } + sum.map(|s| s / n) +} + +/// Zoomed in to compare a detail, every Before / After view still shows the edited photo on its +/// After side. A wipe drew its line through the middle of the photo, which zoomed in is mostly off +/// screen: the whole view was the Before, the original photo (#1351). +#[test] +fn zoomed_in_the_after_side_shows_the_edits() { + let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }); + let mut h = Headless::new(app, [1400.0, 900.0], 1.0); + let r = h.request("ui.set", json!({"view": "detail", "right": "none", "filmstrip": false}), T); + assert_eq!(r["ok"], true, "{r}"); + let r = h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 1.5}}), T); + assert_eq!(r["ok"], true, "{r}"); + let shot = |h: &mut Headless, mode: BeforeAfter, zoom: Zoom| { + h.app.ui.before_after = mode; + h.app.ui.zoom = zoom; + // (far enough from the middle of the photo that, zoomed in, it is off screen) + h.app.ui.pan = (0.25, 0.75); + h.settle(SETTLE); + h.paint() + }; + for zoom in [Zoom::Fit, Zoom::Percent(100.0), Zoom::Percent(400.0)] { + let (edited, original) = (shot(&mut h, BeforeAfter::Off, zoom), shot(&mut h, BeforeAfter::Original, zoom)); + let c = h.app.canvas_rect.expect("canvas").center(); + let luma = |m: [f32; 3]| m.iter().sum::(); + for mode in [BeforeAfter::SideBySide, BeforeAfter::TopBottom, BeforeAfter::Split, BeforeAfter::SplitTopBottom] { + let s = shot(&mut h, mode, zoom); + let canvas = h.app.canvas_rect.expect("canvas"); + // the middle of the After pane, or of the After side of the line + let (after, before) = match mode { + BeforeAfter::SideBySide | BeforeAfter::Split => { + (c + egui::vec2(canvas.width() / 4.0, 0.0), c - egui::vec2(canvas.width() / 4.0, 0.0)) + } + _ => (c + egui::vec2(0.0, canvas.height() / 4.0), c - egui::vec2(0.0, canvas.height() / 4.0)), + }; + let (a, b) = (mean_around(&s, after), mean_around(&s, before)); + if matches!(mode, BeforeAfter::Split | BeforeAfter::SplitTopBottom) { + // a wipe shows the loupe's own pixels on each side of the line + let (e, o) = (mean_around(&edited, after), mean_around(&original, before)); + assert!((luma(a) - luma(e)).abs() < 6.0, "{mode:?} {zoom:?}: the After side {a:?} is the edited photo {e:?}"); + assert!((luma(b) - luma(o)).abs() < 6.0, "{mode:?} {zoom:?}: the Before side {b:?} is the original {o:?}"); + } else { + // two views of the same point of the photo: the After one is the brighter + assert!(luma(a) > luma(b) + 30.0, "{mode:?} {zoom:?}: the After {a:?} is the edited photo, brighter than the Before {b:?}"); + } + } + } +}