diff --git a/crates/pipeline/src/tests_window.rs b/crates/pipeline/src/tests_window.rs index 1d5b15c65..b0fc3de8f 100644 --- a/crates/pipeline/src/tests_window.rs +++ b/crates/pipeline/src/tests_window.rs @@ -238,6 +238,38 @@ fn a_mask_overlay_window_matches_the_whole_render() { } } +// Given the Show All Masks view (issue #1712), a window of it matches the same pixels of the +// whole render with that overlay: the union is composed from the same mask alphas, so the zoomed +// loupe can show it as a window like a single mask's overlay (issue #547) +#[test] +fn an_all_masks_overlay_window_matches_the_whole_render() { + use crate::{MaskView, Overlay}; + use lightcraft_develop::{LocalAdjustments, Mask, MaskComponent, MaskOp, MaskShape}; + use lightcraft_geom::Point; + let shapes = [ + (3, MaskShape::Radial { center: Point::new(0.45, 0.5), rx: 0.2, ry: 0.15, angle: 20.0, feather: 50.0, invert: false }, 0.0), + (4, MaskShape::Linear { start: Point::new(0.4, 0.55), end: Point::new(0.6, 0.5) }, 100.0), + ]; + let mut s = DevelopSettings::default(); + for (id, shape, refine) in shapes { + s.masks.push(Mask { + id, + components: vec![MaskComponent { name: None, op: MaskOp::Add, invert: false, shape }], + adjust: LocalAdjustments { exposure: 1.0, ..Default::default() }, + refine, + ..Default::default() + }); + } + for view in [MaskView::Color, MaskView::WhiteOnBlack, MaskView::ImageOnBlack] { + let overlay = Overlay::AllMasks { view, color: [255, 0, 0], opacity: 70 }; + let f = render(&noisy_scene(), &SourceInfo::default(), &s, &RenderRequest { overlay, ..RenderRequest::fit(W, H) }).image; + let w = render(&noisy_scene(), &SourceInfo::default(), &s, &RenderRequest { overlay, window: Some(MID), ..RenderRequest::fit(W, H) }).image; + let (max, mean) = compare(&f, &w, MID); + // (refine edges differs by a rounding step, as in `mask_edge_refinement_does_not_depend_on_the_window`) + assert!(max <= 3 && mean <= 0.25, "{view:?}: max {max} (≤ 3), mean {mean:.3} (≤ 0.25)"); + } +} + // ---- Deep zoom: the wide stages' kernels are far wider than any margin ------------------------- /// Hard-edged blocks of very different brightness at several scales: the wide stages' edge-aware diff --git a/crates/ui-egui/src/panels/detail.rs b/crates/ui-egui/src/panels/detail.rs index 151d4a57a..d35147628 100644 --- a/crates/ui-egui/src/panels/detail.rs +++ b/crates/ui-egui/src/panels/detail.rs @@ -250,10 +250,12 @@ fn animated_rect(ctx: &egui::Context, anim: &mut bool, target: Rect) -> Rect { } /// Whether an overlay can be rendered on a region tile without changing what the tile means. -/// Mask overlays are evaluated from the same mask alpha as the full render; the other diagnostic -/// overlays either need whole-frame context or deliberately replace the loupe. +/// Mask overlays (one mask, or every visible mask at once) are evaluated from the same mask +/// alphas as the full render; the other diagnostic overlays either need whole-frame context or +/// deliberately replace the loupe. pub(crate) fn overlay_supports_window(overlay: lightcraft_pipeline::Overlay) -> bool { - matches!(overlay, lightcraft_pipeline::Overlay::None | lightcraft_pipeline::Overlay::Mask { .. }) + use lightcraft_pipeline::Overlay; + matches!(overlay, Overlay::None | Overlay::Mask { .. } | Overlay::AllMasks { .. }) } /// What a window render of the loupe needs to know about the view it belongs to. diff --git a/crates/ui-egui/src/tests_preview_limit.rs b/crates/ui-egui/src/tests_preview_limit.rs index 4f058f536..76d3ea29c 100644 --- a/crates/ui-egui/src/tests_preview_limit.rs +++ b/crates/ui-egui/src/tests_preview_limit.rs @@ -227,6 +227,40 @@ mod in_the_loupe { assert_eq!(h.app.region_view.map(|r| r.settings), Some(plain.settings), "back to the plain window"); } + // Given the Show All Masks view (issue #1712) at 1:1, the zoomed loupe is a canvas-sized frame + // plus a native window carrying the overlay, as a selected mask's is: the union is evaluated + // from the same mask alphas, so it must not fall back to a whole-frame render at the zoomed + // size (slower, and its detail stops at the frame render's resolution) + #[test] + fn an_all_masks_overlay_at_one_to_one_is_a_window() { + let (mut h, native) = detail(); + for (command, params) in [ + ("panel.masking", json!({})), + ("mask.add", json!({"kind": "radial", "center": [0.4, 0.5], "rx": 0.2, "ry": 0.2})), + ("mask.add", json!({"kind": "linear", "start": [0.2, 0.3], "end": [0.8, 0.7]})), + ] { + let r = h.request("engine.execute", json!({"command": command, "params": params}), T); + assert_eq!(r["ok"], true, "{command}: {r}"); + } + let r = h.request("engine.execute", json!({"command": "view.maskOverlayAll", "params": {}}), T); + assert_eq!(r["ok"], true, "{r}"); + assert!(h.app.ui.mask_overlay_all, "the all-masks overlay is on"); + h.request("engine.execute", json!({"command": "view.zoom100"}), T); + h.settle(SETTLE); + let id = h.app.session.active().expect("a photo"); + let d = h.app.session.develop_of(id).expect("settings"); + assert!( + matches!(crate::panels::detail::view_overlay(&h.app, &d), lightcraft_pipeline::Overlay::AllMasks { .. }), + "the all-masks overlay is shown" + ); + let main = rendered_long_edge(&h); + assert!(main <= 2560 && main < native, "native {native}: the whole-frame render is {main}"); + let region = h.app.region_view.expect("a window render with the all-masks overlay"); + assert_eq!(region.full.0.max(region.full.1), native, "cut from the photo's own pixels"); + let (w, hh) = region_tile(&h).expect("its texture"); + assert!(w >= 1300 && hh >= 700, "it covers the canvas: {w}×{hh}"); + } + // Given the user capped the preview at 1600 px, then 1:1 is rendered no larger than that #[test] fn an_explicit_limit_still_caps_one_to_one() {