mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
LR-MASK-OVERLAY: test the mask overlay as a zoomed window
There was no zoom/pan reset on main to fix: what this PR changes is that a zoomed view with the mask overlay shown is now a canvas-sized frame plus a sharp window carrying the overlay, as without it, instead of a whole-frame render at the zoomed size. Tests: the overlay on a window matches the same pixels of the whole render for radial, refined linear and sky masks in three views (pipeline tests_window.rs), and at 1:1 the loupe uses a window with the overlay, keeps zoom and pan, and swaps tiles when the overlay is hidden or shown (ui-egui tests_preview_limit.rs). Parity row updated. No render output change (no cache bump). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
760819eab3
commit
0b1d80f08d
3 files changed
+73
-1
No files matched your search
@@ -204,6 +204,40 @@ fn mask_edge_refinement_does_not_depend_on_the_window() {
|
||||
check_noisy("refine", &s, MID, 3, 0.2);
|
||||
}
|
||||
|
||||
// Given a mask overlay (the Masking panel's view of the selected mask), a window of it matches the
|
||||
// same pixels of the whole render with that overlay, for every view and for drawn, AI and
|
||||
// refined shapes: the zoomed loupe can show the overlay as a window (issue #547)
|
||||
#[test]
|
||||
fn a_mask_overlay_window_matches_the_whole_render() {
|
||||
use crate::{MaskView, Overlay};
|
||||
use lightcraft_develop::{LocalAdjustments, Mask, MaskComponent, MaskOp, MaskShape};
|
||||
use lightcraft_geom::Point;
|
||||
let shapes = [
|
||||
("radial", MaskShape::Radial { center: Point::new(0.5, 0.5), rx: 0.2, ry: 0.15, angle: 20.0, feather: 50.0, invert: false }, 0.0),
|
||||
("linear refined", MaskShape::Linear { start: Point::new(0.45, 0.5), end: Point::new(0.55, 0.5) }, 100.0),
|
||||
("sky", MaskShape::Sky, 0.0),
|
||||
];
|
||||
for (name, shape, refine) in shapes {
|
||||
let mut s = DevelopSettings::default();
|
||||
s.masks.push(Mask {
|
||||
id: 3,
|
||||
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::Mask { id: 3, 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, "{name} {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
|
||||
|
||||
@@ -189,6 +189,44 @@ mod in_the_loupe {
|
||||
assert!(w >= 1300 && hh >= 700, "it covers the canvas: {w}×{hh}");
|
||||
}
|
||||
|
||||
// Given a mask selected with its overlay shown at 1:1 (issue #547), the zoomed view keeps its
|
||||
// zoom and pan and stays a canvas-sized frame plus a native window carrying the overlay, as
|
||||
// without it, instead of a whole-frame render at the zoomed size; hiding or showing the
|
||||
// overlay swaps the window for one with the other look
|
||||
#[test]
|
||||
fn a_mask_overlay_at_one_to_one_is_a_window_and_keeps_zoom_and_pan() {
|
||||
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}))] {
|
||||
let r = h.request("engine.execute", json!({"command": command, "params": params}), T);
|
||||
assert_eq!(r["ok"], true, "{command}: {r}");
|
||||
}
|
||||
h.request("engine.execute", json!({"command": "view.zoom100"}), T);
|
||||
h.app.ui.pan = (0.3, 0.4);
|
||||
h.app.ui.mask_overlay = false;
|
||||
h.settle(SETTLE);
|
||||
let (zoom, pan) = (h.app.ui.zoom, h.app.ui.pan);
|
||||
assert_eq!(zoom, crate::state::Zoom::Percent(100.0));
|
||||
let plain = h.app.region_view.expect("a window render without the overlay");
|
||||
|
||||
h.app.ui.mask_overlay = true;
|
||||
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::Mask { .. }), "the mask overlay is shown");
|
||||
assert_eq!((h.app.ui.zoom, h.app.ui.pan), (zoom, pan), "showing the overlay keeps zoom and pan");
|
||||
let main = rendered_long_edge(&h);
|
||||
assert!(main <= 2560 && main < native, "native {native}: the whole-frame render is {main}");
|
||||
let shown = h.app.region_view.expect("a window render with the overlay");
|
||||
assert_eq!(shown.full.0.max(shown.full.1), native, "cut from the photo's own pixels");
|
||||
assert_eq!(shown.window, plain.window, "the same part of the photo");
|
||||
assert_ne!(shown.settings, plain.settings, "the overlay is part of the window's look");
|
||||
|
||||
h.app.ui.mask_overlay = false;
|
||||
h.settle(SETTLE);
|
||||
assert_eq!((h.app.ui.zoom, h.app.ui.pan), (zoom, pan), "hiding it keeps them too");
|
||||
assert_eq!(h.app.region_view.map(|r| r.settings), Some(plain.settings), "back to the plain window");
|
||||
}
|
||||
|
||||
// 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() {
|
||||
|
||||
+1
-1
@@ -365,7 +365,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins.
|
||||
| LR-MASK-AMOUNT | Mask amount | P1 | ✅ | `cmd:mask.adjust` (`amount`), `crates/ui-egui/src/panels/masking.rs` | |
|
||||
| LR-MASK-FEATHER-EDGE | Refine mask edges | P2 | ✅ | `cmd:mask.refine`, `crates/pipeline/src/masks.rs` (`evaluate_one`) | Refine Edges slider per mask: guided filter on the photo's luminance snaps soft mask edges to the photo's edges (rendered on the CPU) |
|
||||
| LR-MASK-SLIDERS | Local adjustment sliders | P0 | ✅ | `cmd:mask.adjust`, `crates/pipeline/src/finish.rs` | every slider renders (CPU + GPU), incl. Noise, Moiré, Defringe (negative Defringe has no effect); no local curve or effect presets |
|
||||
| LR-MASK-OVERLAY | Mask overlay | P0 | ✅ | `cmd:view.maskOverlay`, `cmd:view.maskOverlayMode`, `cmd:view.maskOverlayColor`, `crates/pipeline/src/visualize.rs` | rendered alpha of the selected mask (CPU + GPU): colour, colour on B&W, image on black/white, white on black; ⇧O cycles while masking; successful local adjustments (including Amount) hide the overlay until manually shown again with O / the eye; shape/edge edits retain it; no auto-show on hover |
|
||||
| LR-MASK-OVERLAY | Mask overlay | P0 | ✅ | `cmd:view.maskOverlay`, `cmd:view.maskOverlayMode`, `cmd:view.maskOverlayColor`, `crates/pipeline/src/visualize.rs` | rendered alpha of the selected mask (CPU + GPU): colour, colour on B&W, image on black/white, white on black; ⇧O cycles while masking; successful local adjustments (including Amount) hide the overlay until manually shown again with O / the eye; shape/edge edits retain it; zoomed in, the overlay is rendered on the same sharp window as the plain view (zoom and pan unchanged; `crates/ui-egui/src/tests_preview_limit.rs`, `crates/pipeline/src/tests_window.rs`); no auto-show on hover |
|
||||
| LR-MASK-PINS | Pins | P1 | ✅ | `cmd:view.maskPins`, `crates/ui-egui/src/panels/detail.rs` | a pin per radial/linear/brush component: click selects its mask, drag moves it; no pins for range/AI components, no Auto mode |
|
||||
| LR-MASK-UPDATE | Recompute AI masks | P2 | ⬜ | | |
|
||||
| LR-MASK-SYNC | Copy masks to other photos | P1 | ✅ | `cmd:develop.copy` (`groups`) | |
|
||||
|
||||
Reference in new issue
Block a user