ui: render the Show All Masks overlay in zoom windows

The loupe's window rendering already evaluates every visible mask's alpha
for an `Overlay::AllMasks` (its union is composed from the same alphas the
selected-mask overlay uses), but `overlay_supports_window` refused it, so
at 1:1 and closer "Show All Masks" fell back to a whole-frame render at
the zoomed size: slower, and its detail stopped at the frame render's
resolution. Allowing it makes the zoomed view a canvas-sized frame plus a
native window, as a single mask's overlay already is.

Tests:
- tests_preview_limit::an_all_masks_overlay_at_one_to_one_is_a_window
  (fails before: "native 6000: the whole-frame render is 6000")
- tests_window::an_all_masks_overlay_window_matches_the_whole_render
  (a window of the union matches the whole render's pixels, for every view)
This commit is contained in:
midasmatterhorn
2026-10-15 00:05:03 +03:00
parent 3b4279fc24
commit 0366116c76
3 changed files with 71 additions and 3 deletions
+32
View File
@@ -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
+5 -3
View File
@@ -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.
+34
View File
@@ -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() {