mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
Merge PR #1358
This commit is contained in:
commit
b089188ee5
3 files changed
+89
-12
No files matched your search
@@ -653,19 +653,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)
|
||||
@@ -677,12 +680,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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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::<f32>();
|
||||
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:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user