#323: windows: spots render in a window; deep-zoom regression tests for the wide stages
Spot removal reads outside the spot and its automatic source depends on the pixels at hand. A windowed render now fixes automatic sources from a fixed-size render of the whole frame and grows the window it works on to hold every spot that touches the requested one and the patch it reads (bounded at 8192 px), cutting the result back to the request, so photos with spots get sharp windows too. The wide stages (dehaze, clarity, highlights/shadows) are checked at depth on hard-edged blocks: the cut kernels cost at most a few levels, so they are not computed from a whole-frame proxy. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
dc5bff6947
commit
eebea486d5
|
|
@ -328,6 +328,10 @@ pub struct Plan<'a> {
|
|||
pub eyes: Vec<redeye::EyeK>,
|
||||
/// The whole frame's dehaze airlight, for a windowed render (which can't see the whole frame).
|
||||
pub fixed_air: Option<f32>,
|
||||
/// A windowed render works on a larger window when spots reach into it (see
|
||||
/// [`spots::window_for_spots`]); this is the requested window inside the rendered one, which
|
||||
/// the result is cut to.
|
||||
pub keep: Option<PixelWindow>,
|
||||
}
|
||||
|
||||
/// Long edge of the small render a windowed render estimates the whole frame's airlight from.
|
||||
|
|
@ -340,12 +344,22 @@ pub fn plan<'a>(src: &Rgb32f, info: &SourceInfo, s: &'a DevelopSettings, req: &R
|
|||
Cow::Owned(o) => Cow::Owned(upright::resolve(src, info, &o).into_owned()),
|
||||
};
|
||||
visualize::adjust_settings(req.overlay, &mut settings);
|
||||
// a window must not choose a spot's source from the pixels it happens to hold: fix them
|
||||
// from a fixed-size render of the whole frame first
|
||||
if req.window.is_some() && settings.spots.iter().any(|sp| sp.source_offset.is_none()) {
|
||||
let picked: Vec<Option<lightcraft_geom::Point>> =
|
||||
settings.spots.iter().map(|sp| sp.source_offset.or_else(|| spots::pick_source(src, info, &settings, sp, None))).collect();
|
||||
for (sp, o) in settings.to_mut().spots.iter_mut().zip(picked) {
|
||||
sp.source_offset = o;
|
||||
}
|
||||
}
|
||||
let s = &*settings;
|
||||
let frame = frame_for(src, info, s, req.apply_crop);
|
||||
let (full_w, full_h) = frame.fit(req.max_w, req.max_h);
|
||||
// sizes and scale are those of the whole output; a window only narrows what is drawn
|
||||
let px_per_long = frame.px_per_long(full_w);
|
||||
let mut fixed_air = None;
|
||||
let mut keep = None;
|
||||
let (frame, w, h) = match req.window {
|
||||
Some(win) => {
|
||||
let win = win.clamped(full_w, full_h);
|
||||
|
|
@ -354,7 +368,12 @@ pub fn plan<'a>(src: &Rgb32f, info: &SourceInfo, s: &'a DevelopSettings, req: &R
|
|||
let (pw, ph) = (((full_w as f64 * k).round() as usize).max(1), ((full_h as f64 * k).round() as usize).max(1));
|
||||
fixed_air = Some(local::frame_airlight(src, info, s, &frame, pw, ph));
|
||||
}
|
||||
(frame.window(full_w, full_h, win), win.w, win.h)
|
||||
// (spots grow the rendered window; only 8-bit renders are cut back to the request)
|
||||
let work = if req.depth == OutputDepth::U8 { spots::window_for_spots(&s.spots, &frame, full_w, full_h, px_per_long, win) } else { win };
|
||||
if work != win {
|
||||
keep = Some(PixelWindow { x: win.x - work.x, y: win.y - work.y, ..win });
|
||||
}
|
||||
(frame.window(full_w, full_h, work), work.w, work.h)
|
||||
}
|
||||
None => (frame, full_w, full_h),
|
||||
};
|
||||
|
|
@ -373,7 +392,7 @@ pub fn plan<'a>(src: &Rgb32f, info: &SourceInfo, s: &'a DevelopSettings, req: &R
|
|||
[d.nr_luminance, d.nr_detail, d.nr_color, d.nr_color_detail, d.nr_color_smoothness].map(f64::to_bits),
|
||||
src_long,
|
||||
));
|
||||
Plan { settings, frame, w, h, px_per_long, src_long, geo, lin_key, eyes, fixed_air }
|
||||
Plan { settings, frame, w, h, px_per_long, src_long, geo, lin_key, eyes, fixed_air, keep }
|
||||
}
|
||||
|
||||
/// Whether the scene-linear stage needs work only the CPU does (defringe, spot removal).
|
||||
|
|
@ -472,11 +491,19 @@ fn render_impl(src: Src<'_>, info: &SourceInfo, s: &DevelopSettings, req: &Rende
|
|||
}
|
||||
let image = finish::finish(&prep, s, frame, info, req.space, req.proof);
|
||||
lap("finish", &mut t);
|
||||
let histogram = Histogram::of_srgb8(&image);
|
||||
let cut = |i: &Rgba8| match plan.keep {
|
||||
Some(k) => i.crop(k.x, k.y, k.w, k.h),
|
||||
None => i.clone(),
|
||||
};
|
||||
let histogram = match plan.keep {
|
||||
Some(_) => Histogram::of_srgb8(&cut(&image)),
|
||||
None => Histogram::of_srgb8(&image),
|
||||
};
|
||||
lap("histogram", &mut t);
|
||||
let mut image = image;
|
||||
let mask = overlay_alpha(req.overlay, &plan, &prep);
|
||||
visualize::apply(&mut image, req.overlay, &plan, mask.as_ref());
|
||||
let image = if plan.keep.is_some() { cut(&image) } else { image };
|
||||
Rendered { image, histogram, deep: None }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -106,6 +106,67 @@ pub fn pick_source(
|
|||
Some(Point::new(n1.x - n0.x, n1.y - n0.y))
|
||||
}
|
||||
|
||||
/// The most a window grows to take in the spots that touch it.
|
||||
const MAX_SPOT_SPAN: usize = 8192;
|
||||
|
||||
/// `win` (a window of the `full_w × full_h` output of `frame`) grown to hold, for every spot that
|
||||
/// reaches into it, the whole spot and the patch it reads from: [`apply`] works on the pixels it
|
||||
/// is given, so a spot cut by the window's edge, or whose source lies outside it, would come out
|
||||
/// different from the same spot in the whole render. Spots with an automatic source (`None`)
|
||||
/// search up to 4.2 radii around themselves. Growth stops at [`MAX_SPOT_SPAN`] pixels.
|
||||
pub fn window_for_spots(spots: &[Spot], frame: &Frame, full_w: usize, full_h: usize, ppl: f64, win: crate::PixelWindow) -> crate::PixelWindow {
|
||||
if spots.is_empty() {
|
||||
return win;
|
||||
}
|
||||
let to_out = frame.norm_to_out(full_w, full_h);
|
||||
let origin = to_out.apply(Point::new(0.0, 0.0));
|
||||
// (target box, source box) of each spot, in output pixels: [x0, y0, x1, y1]
|
||||
let boxes: Vec<([f64; 4], [f64; 4])> = spots
|
||||
.iter()
|
||||
.filter_map(|spot| {
|
||||
let r = (spot.size * ppl).max(1.0);
|
||||
let pts: Vec<_> = spot.points.iter().map(|p| to_out.apply(*p)).collect();
|
||||
let first = pts.first()?;
|
||||
let (mut x0, mut y0, mut x1, mut y1) = (first.x, first.y, first.x, first.y);
|
||||
for p in &pts {
|
||||
(x0, y0, x1, y1) = (x0.min(p.x), y0.min(p.y), x1.max(p.x), y1.max(p.y));
|
||||
}
|
||||
// the box `apply` works in: the stroke plus a radius, plus a radius of padding
|
||||
let t = [x0 - 2.0 * r, y0 - 2.0 * r, x1 + 2.0 * r, y1 + 2.0 * r];
|
||||
let s = match spot.source_offset {
|
||||
Some(o) => {
|
||||
let b = to_out.apply(o);
|
||||
let (dx, dy) = (b.x - origin.x, b.y - origin.y);
|
||||
[t[0] + dx, t[1] + dy, t[2] + dx, t[3] + dy]
|
||||
}
|
||||
None => [t[0] - 4.2 * r, t[1] - 4.2 * r, t[2] + 4.2 * r, t[3] + 4.2 * r],
|
||||
};
|
||||
Some((t, s)).filter(|(t, s)| t.iter().chain(s).all(|v| v.is_finite()))
|
||||
})
|
||||
.collect();
|
||||
let (fw, fh) = (full_w as f64, full_h as f64);
|
||||
let mut r = [win.x as f64, win.y as f64, (win.x + win.w) as f64, (win.y + win.h) as f64];
|
||||
let touches = |r: &[f64; 4], b: &[f64; 4]| b[0] < r[2] && b[2] > r[0] && b[1] < r[3] && b[3] > r[1];
|
||||
for _ in 0..=boxes.len() {
|
||||
let mut grown = r;
|
||||
for (t, s) in &boxes {
|
||||
if touches(&r, t) {
|
||||
for b in [t, s] {
|
||||
grown = [grown[0].min(b[0]), grown[1].min(b[1]), grown[2].max(b[2]), grown[3].max(b[3])];
|
||||
}
|
||||
}
|
||||
}
|
||||
let grown = [grown[0].max(0.0), grown[1].max(0.0), grown[2].min(fw), grown[3].min(fh)];
|
||||
if grown == r || grown[2] - grown[0] > MAX_SPOT_SPAN as f64 || grown[3] - grown[1] > MAX_SPOT_SPAN as f64 {
|
||||
break;
|
||||
}
|
||||
r = grown;
|
||||
}
|
||||
let (x, y) = (r[0].floor() as usize, r[1].floor() as usize);
|
||||
crate::PixelWindow { x, y, w: (r[2].ceil() as usize).saturating_sub(x).max(1), h: (r[3].ceil() as usize).saturating_sub(y).max(1) }
|
||||
.clamped(full_w, full_h)
|
||||
}
|
||||
|
||||
/// Apply all spots to `img` (output resolution). `ppl` = output pixels per long-edge unit.
|
||||
pub fn apply(img: &mut Rgb32f, spots: &[Spot], frame: &Frame, ppl: f64) {
|
||||
if spots.is_empty() {
|
||||
|
|
@ -248,4 +309,47 @@ mod tests {
|
|||
assert!(img.get(25, 50)[0] > 0.8);
|
||||
assert!(img.get(5, 50)[0] < 0.2);
|
||||
}
|
||||
|
||||
fn spot_at(x: f64, y: f64, size: f64, offset: Option<Point>) -> Spot {
|
||||
Spot { points: vec![Point::new(x, y)], size, source_offset: offset, ..Default::default() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_grows_to_hold_the_spots_that_touch_it_and_their_sources() {
|
||||
use crate::PixelWindow;
|
||||
let frame = Frame::new(4000, 3000, &DevelopSettings::default(), true);
|
||||
let ppl = frame.px_per_long(4000);
|
||||
let win = PixelWindow { x: 1000, y: 1000, w: 400, h: 300 };
|
||||
// none: the window is left alone
|
||||
assert_eq!(window_for_spots(&[], &frame, 4000, 3000, ppl, win), win);
|
||||
// a spot far away is no business of this window
|
||||
let far = spot_at(0.9, 0.9, 0.01, Some(Point::new(0.02, 0.0)));
|
||||
assert_eq!(window_for_spots(&[far], &frame, 4000, 3000, ppl, win), win);
|
||||
// a spot inside it, reading from 300 px to the right, widens it to the source
|
||||
let near = spot_at(0.30, 0.38, 0.01, Some(Point::new(0.075, 0.0)));
|
||||
let g = window_for_spots(&[near], &frame, 4000, 3000, ppl, win);
|
||||
assert!(g.x <= win.x && g.x + g.w >= 1200 + 300 + 80 && g.y <= win.y && g.y + g.h >= win.y + win.h, "{g:?}");
|
||||
// a spot that only reaches in with its edge counts too
|
||||
let edge = spot_at(0.24, 0.38, 0.01, Some(Point::new(-0.05, 0.0)));
|
||||
assert!(window_for_spots(&[edge], &frame, 4000, 3000, ppl, win).x < win.x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spot_windows_are_bounded_and_survive_hostile_spots() {
|
||||
use crate::PixelWindow;
|
||||
let frame = Frame::new(40_000, 30_000, &DevelopSettings::default(), true);
|
||||
let ppl = frame.px_per_long(40_000);
|
||||
let win = PixelWindow { x: 1000, y: 1000, w: 400, h: 300 };
|
||||
// a spot whose source is a long way off would need an enormous window: not granted
|
||||
let wide = spot_at(0.03, 0.04, 0.01, Some(Point::new(0.6, 0.0)));
|
||||
let g = window_for_spots(&[wide], &frame, 40_000, 30_000, ppl, win);
|
||||
assert!(g.w <= MAX_SPOT_SPAN && g.h <= MAX_SPOT_SPAN, "{g:?}");
|
||||
for bad in [f64::NAN, f64::INFINITY, 1e300, -1e300] {
|
||||
let s = spot_at(bad, bad, bad, Some(Point::new(bad, bad)));
|
||||
let g = window_for_spots(&[s], &frame, 40_000, 30_000, ppl, win);
|
||||
assert_eq!(g, win);
|
||||
}
|
||||
let empty = Spot { points: vec![], ..Default::default() };
|
||||
assert_eq!(window_for_spots(&[empty], &frame, 40_000, 30_000, ppl, win), win);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,9 +29,14 @@ fn window(s: &DevelopSettings, win: PixelWindow) -> Rgba8 {
|
|||
/// Largest per-channel difference and mean absolute difference between `win` (rendered for
|
||||
/// `at`, kept `HALO` px in from its edges) and the same pixels of `full`.
|
||||
fn compare(full: &Rgba8, win: &Rgba8, at: PixelWindow) -> (u8, f64) {
|
||||
compare_inset(full, win, at, HALO)
|
||||
}
|
||||
|
||||
/// [`compare`] keeping `inset` px in from the window's edges.
|
||||
fn compare_inset(full: &Rgba8, win: &Rgba8, at: PixelWindow, inset: usize) -> (u8, f64) {
|
||||
let (mut max, mut sum, mut n) = (0u8, 0f64, 0f64);
|
||||
for y in HALO..at.h - HALO {
|
||||
for x in HALO..at.w - HALO {
|
||||
for y in inset..at.h - inset {
|
||||
for x in inset..at.w - inset {
|
||||
let (a, b) = (full.get(at.x + x, at.y + y), win.get(x, y));
|
||||
for k in 0..3 {
|
||||
let d = a[k].abs_diff(b[k]);
|
||||
|
|
@ -198,3 +203,115 @@ fn mask_edge_refinement_does_not_depend_on_the_window() {
|
|||
});
|
||||
check_noisy("refine", &s, MID, 3, 0.2);
|
||||
}
|
||||
|
||||
// ---- 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
|
||||
/// filters and the dark channel have something to be wrong about.
|
||||
fn blocks() -> Rgb32f {
|
||||
Rgb32f::from_fn(600, 400, |x, y| {
|
||||
let cell = |size: usize, k: usize| ((x / size * 7 + y / size * 13 + k) % 5) as f32 / 4.0;
|
||||
let l = 0.02 + 0.5 * cell(97, 1) + 0.25 * cell(31, 2) + 0.1 * cell(11, 3);
|
||||
[l * 1.1, l, l * 0.8]
|
||||
})
|
||||
}
|
||||
|
||||
const BIG_W: usize = 7680;
|
||||
const BIG_H: usize = 5120;
|
||||
|
||||
/// A window of a 7680 × 5120 frame against the same pixels of the whole render. The wide stages
|
||||
/// (dehaze, clarity, highlights/shadows base) have XX/// can't contain them, so they come from the whole frame at reduced size.
|
||||
fn check_big(name: &str, s: &DevelopSettings, max_ok: u8, mean_ok: f64) {
|
||||
let src = blocks();
|
||||
let f = render(&src, &SourceInfo::default(), s, &RenderRequest::fit(BIG_W, BIG_H)).image;
|
||||
let win = PixelWindow { x: 3000, y: 1800, w: 512, h: 384 };
|
||||
let w = render(&src, &SourceInfo::default(), s, &RenderRequest { window: Some(win), ..RenderRequest::fit(BIG_W, BIG_H) }).image;
|
||||
let (max, mean) = compare(&f, &w, win);
|
||||
assert!(max <= max_ok && mean <= mean_ok, "{name}: max {max} (≤ {max_ok}), mean {mean:.3} (≤ {mean_ok})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dehaze_at_depth_matches_the_whole_render() {
|
||||
let mut s = DevelopSettings::default();
|
||||
controls::set(&mut s, "effects.dehaze", 60.0);
|
||||
check_big("dehaze", &s, 6, 0.6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clarity_at_depth_matches_the_whole_render() {
|
||||
let mut s = DevelopSettings::default();
|
||||
controls::set(&mut s, "effects.clarity", 80.0);
|
||||
check_big("clarity", &s, 6, 0.6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn highlights_and_shadows_at_depth_match_the_whole_render() {
|
||||
let mut s = DevelopSettings::default();
|
||||
controls::set(&mut s, "light.shadows", 80.0);
|
||||
controls::set(&mut s, "light.highlights", -80.0);
|
||||
check_big("shadows+highlights", &s, 6, 0.6);
|
||||
}
|
||||
|
||||
// ---- Spot removal reads outside the spot -------------------------------------------------------
|
||||
|
||||
/// Like [`check`], over every pixel of the window (a spot at its edge must be right too).
|
||||
fn check_whole(name: &str, s: &DevelopSettings, win: PixelWindow, max_ok: u8, mean_ok: f64) {
|
||||
let (f, w) = (full(s), window(s, win));
|
||||
let (max, mean) = compare_inset(&f, &w, win, 0);
|
||||
assert_eq!((w.width, w.height), (win.w, win.h), "{name}: the window's own size");
|
||||
assert!(max <= max_ok && mean <= mean_ok, "{name}: max {max} (≤ {max_ok}), mean {mean:.3} (≤ {mean_ok})");
|
||||
}
|
||||
|
||||
fn with_spot(mode: lightcraft_develop::SpotMode, at: (f64, f64), offset: Option<(f64, f64)>) -> DevelopSettings {
|
||||
use lightcraft_develop::Spot;
|
||||
use lightcraft_geom::Point;
|
||||
let mut s = DevelopSettings::default();
|
||||
s.spots.push(Spot {
|
||||
mode,
|
||||
points: vec![Point::new(at.0, at.1)],
|
||||
size: 0.04,
|
||||
feather: 30.0,
|
||||
opacity: 100.0,
|
||||
source_offset: offset.map(|(x, y)| Point::new(x, y)),
|
||||
});
|
||||
s
|
||||
}
|
||||
|
||||
// Given a spot whose source patch lies outside the window, the window still shows it healed
|
||||
#[test]
|
||||
fn a_spot_with_its_source_outside_the_window_matches_the_whole_render() {
|
||||
use lightcraft_develop::SpotMode;
|
||||
// the target is at x ≈ 0.36 of the frame; its source is 0.12 of the width to the right
|
||||
for mode in [SpotMode::Clone, SpotMode::Heal] {
|
||||
let s = with_spot(mode, (0.40, 0.5), Some((0.12, 0.05)));
|
||||
// a window that holds the target (x ≈ 384) but not the source (x ≈ 499..)
|
||||
check_whole(&format!("{mode:?}"), &s, PixelWindow { x: 250, y: 200, w: 200, h: 200 }, 1, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Given a spot just outside the window whose feathered edge reaches in, the window shows it
|
||||
#[test]
|
||||
fn a_spot_overlapping_the_window_edge_matches_the_whole_render() {
|
||||
let s = with_spot(lightcraft_develop::SpotMode::Heal, (0.30, 0.5), Some((-0.1, 0.0)));
|
||||
check_whole("edge", &s, PixelWindow { x: 300, y: 200, w: 240, h: 240 }, 1, 0.1);
|
||||
}
|
||||
|
||||
// Given a spot with an automatic source, two windows over the same spot choose the same source
|
||||
#[test]
|
||||
fn an_automatic_spot_source_does_not_depend_on_the_window() {
|
||||
let s = with_spot(lightcraft_develop::SpotMode::Heal, (0.5, 0.5), None);
|
||||
// windows so tight that the candidate sources can't all be inside them
|
||||
let (a, b) = (PixelWindow { x: 430, y: 280, w: 110, h: 90 }, PixelWindow { x: 450, y: 290, w: 110, h: 90 });
|
||||
let (ia, ib) = (window(&s, a), window(&s, b));
|
||||
let (mut max, mut n) = (0u8, 0);
|
||||
for y in 0..80 {
|
||||
for x in 0..90 {
|
||||
let (p, q) = (ia.get(x + 20, y + 10), ib.get(x, y));
|
||||
for k in 0..3 {
|
||||
max = max.max(p[k].abs_diff(q[k]));
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(n > 0 && max <= 2, "overlapping windows disagree by up to {max}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,8 +355,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||
&& view_overlay(app, &d) == lightcraft_pipeline::Overlay::None;
|
||||
let drawn_long = target_rect.width().max(target_rect.height()) * ppp;
|
||||
let settled_edge = app.ui.settings.loupe_edge(drawn_long, native_long);
|
||||
let region_possible =
|
||||
plain_view && app.renderer.can_render_windows() && crate::region::windows_allowed(&d) && crate::region::needed(drawn_long, settled_edge);
|
||||
let region_possible = plain_view && app.renderer.can_render_windows() && crate::region::needed(drawn_long, settled_edge);
|
||||
let look = d.hash64();
|
||||
let mut region_full = None;
|
||||
if region_possible {
|
||||
|
|
|
|||
|
|
@ -35,13 +35,6 @@ impl RegionView {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether windows can be rendered for these settings. Spot removal reads its source patch from
|
||||
/// outside the spot, and an automatic one is chosen from the pixels at hand: neither is the same
|
||||
/// in a window as in the whole frame, so those photos stay on the whole-frame render.
|
||||
pub fn windows_allowed(s: &lightcraft_develop::DevelopSettings) -> bool {
|
||||
s.spots.is_empty()
|
||||
}
|
||||
|
||||
/// Context to render around the visible pixels of a frame `full_long` pixels along: enough for the
|
||||
/// wide stages (base, clarity and dehaze planes have sigmas up to 0.02 of it) up to a cap.
|
||||
pub fn margin_for(full_long: usize) -> usize {
|
||||
|
|
@ -152,16 +145,6 @@ mod tests {
|
|||
assert!(!v.is_current(PhotoId(1), (6000, 4000), 12), "another look (an edit, an undo)");
|
||||
}
|
||||
|
||||
// Given a photo with spot removal, no window render: a spot reads pixels beyond the window
|
||||
#[test]
|
||||
fn photos_with_spots_stay_on_the_whole_frame_render() {
|
||||
use lightcraft_develop::{DevelopSettings, Spot};
|
||||
let mut s = DevelopSettings::default();
|
||||
assert!(windows_allowed(&s));
|
||||
s.spots.push(Spot::default());
|
||||
assert!(!windows_allowed(&s));
|
||||
}
|
||||
|
||||
// Given nothing visible, or hostile numbers, there is no window (never a panic)
|
||||
#[test]
|
||||
fn nothing_visible_or_hostile_input_is_no_window() {
|
||||
|
|
|
|||
|
|
@ -276,4 +276,23 @@ mod in_the_loupe {
|
|||
assert_ne!(first.key, second.key);
|
||||
assert_eq!(h.app.renderer.textures.get(&Slot::Region).map(|t| t.key), Some(second.key), "the new window arrived");
|
||||
}
|
||||
|
||||
// Given a photo with spot removal, a zoomed view still gets its sharp window
|
||||
#[test]
|
||||
fn a_photo_with_spots_gets_a_window_too() {
|
||||
let (mut h, _) = detail();
|
||||
let id = h.app.session.active().unwrap();
|
||||
let mut s = (*h.app.session.develop_of(id).unwrap()).clone();
|
||||
s.spots.push(lightcraft_develop::Spot {
|
||||
points: vec![lightcraft_geom::Point::new(0.5, 0.5)],
|
||||
size: 0.01,
|
||||
source_offset: Some(lightcraft_geom::Point::new(0.05, 0.0)),
|
||||
..Default::default()
|
||||
});
|
||||
h.app.session.set_develop(id, s, "Spot").unwrap();
|
||||
h.app.ui.settings.preview_limit = 1600;
|
||||
h.request("engine.execute", json!({"command": "view.zoom100"}), T);
|
||||
h.settle(SETTLE);
|
||||
assert!(region_tile(&h).is_some(), "a window render");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue