mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 20:17:18 -06:00
#323: the loupe renders the canvas, plus the visible window at no more than 100 %
At 8:1 on a 24 MP photo every drag frame rendered the whole 24 MP frame, and a Retina fit view (about 2800 px) decoded the original of every photo stepped to. A new render plan (region::plan, pure and tested) makes the cost follow the canvas, not the zoom: - the whole-frame render is canvas-sized and never above the preview source level, the preview size limit or the texture side (a drag drafts that); - once the view is zoomed past it (by more than 25 %) the visible window is rendered, cut from a frame that is at most the photo's own pixels, so beyond 100 % the GPU magnifies it instead of a bigger render being made; this also ends the duplicated second render above 100 %; - a window is drafted during a drag and a draft of an older value still draws; - a user who capped the preview size keeps the whole-frame render at fit; - no window where the picture on screen isn't the frame's own aspect (an unsupported raw's embedded JPEG cropped differently, #280). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
b7f632f5c0
commit
3255cda67e
@@ -298,7 +298,19 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
let scale = if interacting { 0.6 } else { 1.0 };
|
||||
// render at the final size: a click-zoom animation only changes how the result is drawn
|
||||
let native_long = native[0].max(native[1]);
|
||||
let want = app.ui.settings.loupe_edge(target_rect.width().max(target_rect.height()) * ppp * scale, native_long, texture_side);
|
||||
let drawn_long = target_rect.width().max(target_rect.height()) * ppp;
|
||||
// the whole frame at about canvas size; the zoomed part of it is a window render (below)
|
||||
let plan = crate::region::plan(
|
||||
&app.ui.settings,
|
||||
crate::region::ViewSizes {
|
||||
drawn_long,
|
||||
canvas_long: canvas.width().max(canvas.height()) * ppp,
|
||||
native_long,
|
||||
texture_side,
|
||||
draft_scale: scale,
|
||||
},
|
||||
);
|
||||
let want = plan.main_edge;
|
||||
let (rw, rh) = if aspect >= 1.0 { (want, (want as f32 / aspect) as usize) } else { ((want as f32 * aspect) as usize, want) };
|
||||
// hover and before renders are stand-ins: the preview size is plenty and keeps them cheap
|
||||
let se = app.ui.settings.stand_in_edge(want, texture_side);
|
||||
@@ -352,39 +364,39 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
{
|
||||
app.renderer.request(Slot::Before, job, 90);
|
||||
}
|
||||
// zoomed past what the whole-frame render holds: also render the part on screen at the zoom
|
||||
// scale (not while comparing, hovering a look or showing an overlay: those draw one image; and
|
||||
// not during a drag, whose draft is what shows until it ends)
|
||||
// zoomed past what the whole-frame render holds: also render the part on screen, at no more
|
||||
// than 100 % (the GPU magnifies beyond that). Not while comparing, hovering a look or showing
|
||||
// an overlay (those draw one image), nor where the shown picture isn't the frame's own (an
|
||||
// unsupported raw's embedded JPEG with another crop: window coordinates are the frame's).
|
||||
let plain_view = !split
|
||||
&& !show_before
|
||||
&& !split_view
|
||||
&& hover_key.is_none()
|
||||
&& !app.ui.soft_proof
|
||||
&& 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, texture_side);
|
||||
let region_possible = plain_view && crate::region::needed(drawn_long, settled_edge);
|
||||
&& view_overlay(app, &d) == lightcraft_pipeline::Overlay::None
|
||||
&& crate::region::same_aspect(display_aspect, aspect);
|
||||
let look = d.hash64();
|
||||
let mut region_full = None;
|
||||
if region_possible {
|
||||
let (fw, fh) = if aspect >= 1.0 { (drawn_long, drawn_long / aspect) } else { (drawn_long * aspect, drawn_long) };
|
||||
if let Some(frame_edge) = plan.window_edge.filter(|_| plain_view) {
|
||||
let (fw, fh) = if aspect >= 1.0 { (frame_edge as f32, frame_edge as f32 / aspect) } else { (frame_edge as f32 * aspect, frame_edge as f32) };
|
||||
let (fw, fh) = (fw.round().max(1.0) as usize, fh.round().max(1.0) as usize);
|
||||
region_full = Some((fw, fh));
|
||||
// what is on screen, in pixels of the zoomed frame
|
||||
let to_px = |v: f32| v * ppp;
|
||||
// what is on screen, in pixels of the window's frame (the drawn frame, scaled)
|
||||
let to_px = ppp * frame_edge as f32 / drawn_long.max(1.0);
|
||||
let visible = (
|
||||
to_px(canvas.left() - target_rect.left()),
|
||||
to_px(canvas.top() - target_rect.top()),
|
||||
to_px(canvas.right() - target_rect.left()),
|
||||
to_px(canvas.bottom() - target_rect.top()),
|
||||
to_px * (canvas.left() - target_rect.left()),
|
||||
to_px * (canvas.top() - target_rect.top()),
|
||||
to_px * (canvas.right() - target_rect.left()),
|
||||
to_px * (canvas.bottom() - target_rect.top()),
|
||||
);
|
||||
if !interacting
|
||||
&& let Some(win) = crate::region::window_for(fw, fh, visible, crate::region::max_span(texture_side))
|
||||
if let Some(win) = crate::region::window_for(fw, fh, visible, crate::region::max_span(texture_side))
|
||||
&& let Some(job) = app.session.region_job(id, fw, fh, win, !crop_tool)
|
||||
{
|
||||
// a drag renders drafts of the window, as it does of the whole frame
|
||||
let job = if interacting { job.draft() } else { job };
|
||||
let view = crate::region::RegionView { photo: id, key: job.key, full: (fw, fh), window: win, settings: look };
|
||||
// windows of other photos, looks and zooms can't be drawn any more
|
||||
app.region_tiles.retain(|_, v| v.is_current(id, (fw, fh), look));
|
||||
app.region_tiles.retain(|_, v| v.is_current(id, (fw, fh), look, interacting));
|
||||
app.region_tiles.insert(job.key, view);
|
||||
app.region_view = Some(view);
|
||||
app.renderer.request(Slot::Region, job, 99);
|
||||
@@ -461,7 +473,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
||||
// the window render, drawn over the whole-frame one where it belongs in the frame
|
||||
if let Some(full) = region_full
|
||||
&& let Some(tex) = app.renderer.textures.get(&Slot::Region).filter(|t| t.photo == id)
|
||||
&& let Some(v) = app.region_tiles.get(&tex.key).filter(|v| v.is_current(id, full, look))
|
||||
&& let Some(v) = app.region_tiles.get(&tex.key).filter(|v| v.is_current(id, full, look, interacting))
|
||||
{
|
||||
let (fw, fh) = (v.full.0 as f32, v.full.1 as f32);
|
||||
let at = img_rect.min + vec2(v.window.x as f32 / fw * img_rect.width(), v.window.y as f32 / fh * img_rect.height());
|
||||
|
||||
@@ -30,8 +30,11 @@ pub struct RegionView {
|
||||
impl RegionView {
|
||||
/// Whether this window's pixels still belong in a loupe showing `photo` with `settings` in a
|
||||
/// frame of `full` pixels (they are drawn until the next window arrives, if so).
|
||||
pub fn is_current(&self, photo: lightcraft_catalog::PhotoId, full: (usize, usize), settings: u64) -> bool {
|
||||
self.photo == photo && self.full == full && self.settings == settings
|
||||
///
|
||||
/// While a slider is dragged (`drafting`) a window made for an earlier value of it still
|
||||
/// belongs: drafts follow the drag, and waiting for an exact match would show none.
|
||||
pub fn is_current(&self, photo: lightcraft_catalog::PhotoId, full: (usize, usize), settings: u64, drafting: bool) -> bool {
|
||||
self.photo == photo && self.full == full && (drafting || self.settings == settings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +52,61 @@ pub fn max_span(texture_side: usize) -> usize {
|
||||
(texture_side.min(MAX_SPAN) / SNAP * SNAP).max(SNAP)
|
||||
}
|
||||
|
||||
/// A window render is worth it only when the whole-frame render is this much smaller than the
|
||||
/// zoomed frame: a little softness (a Retina fit view of 2800 px from the 2560 px preview) is not
|
||||
/// worth decoding the original for.
|
||||
pub const WINDOW_RATIO: f32 = 1.25;
|
||||
|
||||
/// How big the loupe's view of the photo is, and what the host allows.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ViewSizes {
|
||||
/// Long edge of the photo as drawn (physical px): the zoomed frame.
|
||||
pub drawn_long: f32,
|
||||
/// Long edge of the canvas it is drawn in (physical px).
|
||||
pub canvas_long: f32,
|
||||
/// The photo's own long edge as shown (after crop and rotation).
|
||||
pub native_long: usize,
|
||||
/// The largest texture the GPU takes.
|
||||
pub texture_side: usize,
|
||||
/// 1.0, or less while a slider is dragged (a draft of the whole-frame render).
|
||||
pub draft_scale: f32,
|
||||
}
|
||||
|
||||
/// What the loupe renders for a view: the whole frame at about canvas size, and, once the view is
|
||||
/// zoomed past what that holds, the window on screen at no more than 100 %.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct LoupePlan {
|
||||
/// Long edge of the whole-frame render ([`Slot::Main`](crate::render::Slot::Main)): never more
|
||||
/// than the canvas shows, the user's preview limit, the texture side or the preview source level.
|
||||
pub main_edge: usize,
|
||||
/// Long edge of the zoomed frame the window is cut from (never above the photo's own pixels:
|
||||
/// beyond 100 % the GPU magnifies the window), or `None`: the whole-frame render is enough.
|
||||
pub window_edge: Option<usize>,
|
||||
}
|
||||
|
||||
/// The loupe's render plan. The cost of a render follows the canvas, not the zoom: the whole-frame
|
||||
/// render is canvas-sized, and the window holds only the pixels on screen at no more than 100 %.
|
||||
pub fn plan(settings: &crate::state::AppSettings, v: ViewSizes) -> LoupePlan {
|
||||
let canvas_long = if v.canvas_long.is_finite() { v.canvas_long.max(8.0) } else { 8.0 };
|
||||
let drawn_long = if v.drawn_long.is_finite() { v.drawn_long.max(8.0) } else { canvas_long };
|
||||
let scale = if v.draft_scale.is_finite() { v.draft_scale.clamp(0.1, 1.0) } else { 1.0 };
|
||||
let main_edge = settings
|
||||
.loupe_edge(drawn_long.min(canvas_long) * scale, v.native_long, v.texture_side)
|
||||
.min(lightcraft_engine::SourceLevel::Preview.max_edge());
|
||||
let frame_edge = settings.window_frame_edge(drawn_long, v.native_long);
|
||||
// a user who capped the preview size has chosen speed over sharpness at fit
|
||||
let fit = drawn_long <= canvas_long * 1.05;
|
||||
let window_edge = (frame_edge as f32 > main_edge as f32 / scale * WINDOW_RATIO && !(fit && settings.preview_limit != 0)).then_some(frame_edge);
|
||||
LoupePlan { main_edge, window_edge }
|
||||
}
|
||||
|
||||
/// Whether the picture on screen has the frame's aspect (to the one pixel a texture's whole-pixel
|
||||
/// size is off by): a window is a part of the frame, so it can only be placed over a picture that
|
||||
/// is the frame (an unsupported raw's embedded JPEG may be cropped differently).
|
||||
pub fn same_aspect(shown: f32, frame: f32) -> bool {
|
||||
shown.is_finite() && frame.is_finite() && frame > 0.0 && (shown / frame - 1.0).abs() <= 0.02
|
||||
}
|
||||
|
||||
/// Whether a view drawn `drawn_long` pixels along its long edge needs a window render on top of
|
||||
/// the whole-frame render, which has only `rendered_long` of them.
|
||||
pub fn needed(drawn_long: f32, rendered_long: usize) -> bool {
|
||||
@@ -87,6 +145,110 @@ pub fn window_for(full_w: usize, full_h: usize, visible: (f32, f32, f32, f32), m
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::AppSettings;
|
||||
|
||||
const BIG: usize = 16384;
|
||||
|
||||
fn sizes(drawn: f32, canvas: f32, native: usize) -> ViewSizes {
|
||||
ViewSizes { drawn_long: drawn, canvas_long: canvas, native_long: native, texture_side: BIG, draft_scale: 1.0 }
|
||||
}
|
||||
|
||||
fn auto() -> AppSettings {
|
||||
AppSettings::default()
|
||||
}
|
||||
|
||||
// Given a fit view on a Retina Mac (about 2800 px), the preview is enough: no original decoded
|
||||
#[test]
|
||||
fn a_retina_fit_view_needs_no_window() {
|
||||
let p = plan(&auto(), sizes(2800.0, 2800.0, 6000));
|
||||
assert_eq!(p.main_edge, 2560, "the preview source level");
|
||||
assert_eq!(p.window_edge, None);
|
||||
}
|
||||
|
||||
// Given a 4K display, the fit view is 1.5x the preview: the window makes it sharp
|
||||
#[test]
|
||||
fn a_large_fit_view_gets_a_window() {
|
||||
let p = plan(&auto(), sizes(3840.0, 3840.0, 6000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (2560, Some(3840)));
|
||||
}
|
||||
|
||||
// Given 1:1 on a 24 MP photo in a 2800 px canvas, the whole frame stays canvas-sized and the
|
||||
// window is cut from the photo's own pixels
|
||||
#[test]
|
||||
fn one_to_one_renders_the_canvas_and_a_native_window() {
|
||||
let p = plan(&auto(), sizes(6000.0, 2800.0, 6000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (2560, Some(6000)));
|
||||
}
|
||||
|
||||
// Given 800 %, the window is still at 100 % (the GPU magnifies it): the cost does not grow
|
||||
#[test]
|
||||
fn beyond_one_to_one_the_window_stays_at_native_resolution() {
|
||||
let p = plan(&auto(), sizes(48000.0, 2800.0, 6000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (2560, Some(6000)));
|
||||
let q = plan(&auto(), sizes(480.0 * 100.0, 2800.0, 6000));
|
||||
assert_eq!(q.window_edge, Some(6000));
|
||||
}
|
||||
|
||||
// Given a small photo zoomed in, the whole-frame render already holds all its pixels
|
||||
#[test]
|
||||
fn a_small_photo_needs_no_window() {
|
||||
let p = plan(&auto(), sizes(8000.0, 1400.0, 1000));
|
||||
assert_eq!((p.main_edge, p.window_edge), (1000, None));
|
||||
}
|
||||
|
||||
// Given a slider drag at 400 % (drafts at 0.6 scale), the window plan does not change: it is
|
||||
// the whole-frame draft that gets cheaper
|
||||
#[test]
|
||||
fn a_drag_shrinks_the_whole_frame_draft_not_the_window() {
|
||||
let still = plan(&auto(), sizes(24000.0, 2800.0, 6000));
|
||||
let drag = plan(&auto(), ViewSizes { draft_scale: 0.6, ..sizes(24000.0, 2800.0, 6000) });
|
||||
assert!(drag.main_edge < still.main_edge, "{drag:?} vs {still:?}");
|
||||
assert_eq!(drag.window_edge, still.window_edge);
|
||||
}
|
||||
|
||||
// Given the user capped the preview size, the whole-frame render obeys it, and a fit view
|
||||
// stays on it (no original decoded); a zoomed view still gets its sharp window
|
||||
#[test]
|
||||
fn a_preview_limit_caps_the_whole_frame_render_and_spares_the_fit_view() {
|
||||
let s = AppSettings { preview_limit: 1600, ..Default::default() };
|
||||
let fit = plan(&s, sizes(2800.0, 2800.0, 6000));
|
||||
assert_eq!((fit.main_edge, fit.window_edge), (1600, None));
|
||||
let zoomed = plan(&s, sizes(6000.0, 2800.0, 6000));
|
||||
assert_eq!((zoomed.main_edge, zoomed.window_edge), (1600, Some(6000)));
|
||||
}
|
||||
|
||||
// Given a GPU with 2048 px textures, the whole-frame render obeys it, the window frame need not
|
||||
// (a window is cut to the texture side separately)
|
||||
#[test]
|
||||
fn the_texture_side_caps_the_whole_frame_render_only() {
|
||||
let p = plan(&auto(), ViewSizes { texture_side: 2048, ..sizes(6000.0, 2800.0, 6000) });
|
||||
assert_eq!((p.main_edge, p.window_edge), (2048, Some(6000)));
|
||||
}
|
||||
|
||||
// Given a picture that is not the frame (an embedded JPEG cropped differently), no window
|
||||
#[test]
|
||||
fn a_window_needs_a_picture_with_the_frames_aspect() {
|
||||
assert!(same_aspect(1.5, 1.5) && same_aspect(1.51, 1.5));
|
||||
assert!(!same_aspect(1.5, 1.0) && !same_aspect(f32::NAN, 1.0) && !same_aspect(1.0, 0.0));
|
||||
}
|
||||
|
||||
// Given a drag at fit on a Retina Mac, the window does not switch on (it would flicker with the drag)
|
||||
#[test]
|
||||
fn a_drag_at_fit_does_not_switch_the_window_on() {
|
||||
let p = plan(&auto(), ViewSizes { draft_scale: 0.6, ..sizes(2800.0, 2800.0, 6000) });
|
||||
assert_eq!(p.window_edge, None);
|
||||
}
|
||||
|
||||
// Hostile numbers give a plan, never a panic
|
||||
#[test]
|
||||
fn hostile_sizes_give_a_usable_plan() {
|
||||
for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, -3.0, 0.0] {
|
||||
let p = plan(&auto(), ViewSizes { drawn_long: v, canvas_long: v, draft_scale: v, ..sizes(1.0, 1.0, 6000) });
|
||||
assert!((8..=2560).contains(&p.main_edge), "{v}: {p:?}");
|
||||
}
|
||||
let p = plan(&auto(), sizes(1000.0, 1000.0, 0));
|
||||
assert!(p.main_edge >= 8);
|
||||
}
|
||||
|
||||
// Given a view drawn at 6000 px and a render of 6000, no window is needed; at 6000 vs 2560 it is
|
||||
#[test]
|
||||
@@ -147,10 +309,12 @@ mod tests {
|
||||
fn a_tile_is_drawn_only_while_it_shows_what_the_loupe_shows() {
|
||||
use lightcraft_catalog::PhotoId;
|
||||
let v = RegionView { photo: PhotoId(1), key: 7, full: (6000, 4000), window: PixelWindow { x: 0, y: 0, w: 256, h: 256 }, settings: 11 };
|
||||
assert!(v.is_current(PhotoId(1), (6000, 4000), 11));
|
||||
assert!(!v.is_current(PhotoId(2), (6000, 4000), 11), "another photo");
|
||||
assert!(!v.is_current(PhotoId(1), (6000, 4001), 11), "another zoom");
|
||||
assert!(!v.is_current(PhotoId(1), (6000, 4000), 12), "another look (an edit, an undo)");
|
||||
assert!(v.is_current(PhotoId(1), (6000, 4000), 11, false));
|
||||
assert!(!v.is_current(PhotoId(2), (6000, 4000), 11, false), "another photo");
|
||||
assert!(!v.is_current(PhotoId(1), (6000, 4001), 11, false), "another zoom");
|
||||
assert!(!v.is_current(PhotoId(1), (6000, 4000), 12, false), "another look (an edit, an undo)");
|
||||
assert!(v.is_current(PhotoId(1), (6000, 4000), 12, true), "a drag's drafts follow the value");
|
||||
assert!(!v.is_current(PhotoId(2), (6000, 4000), 12, true), "…of this photo only");
|
||||
}
|
||||
|
||||
// Given a GPU that allows 2048 px textures, no window is wider than that
|
||||
|
||||
@@ -218,6 +218,15 @@ impl AppSettings {
|
||||
loupe_edge.min(lightcraft_engine::SourceLevel::Preview.max_edge()).min(texture_side.max(MIN_TEXTURE_SIDE))
|
||||
}
|
||||
|
||||
/// Long edge of the zoomed frame a window render is cut from, for a photo drawn `drawn_long`
|
||||
/// px along its long edge: as drawn, but never above the photo's own pixels (beyond 100 % the
|
||||
/// GPU magnifies the window) or the ceiling. The preview size limit is not applied: it is about
|
||||
/// the whole-frame render (see [`crate::region::plan`]).
|
||||
pub fn window_frame_edge(&self, drawn_long: f32, native_long_edge: usize) -> usize {
|
||||
let wanted = if drawn_long.is_nan() { 8.0 } else { drawn_long.clamp(8.0, LOUPE_EDGE_CEILING as f32) } as usize;
|
||||
wanted.min(native_long_edge.max(8)).min(LOUPE_EDGE_CEILING as usize)
|
||||
}
|
||||
|
||||
/// The edge Build Standard-Sized Previews uses.
|
||||
pub fn standard_preview_edge(&self) -> u32 {
|
||||
if self.preview_limit == 0 { STANDARD_PREVIEW_EDGE } else { self.preview_limit }
|
||||
|
||||
@@ -169,15 +169,20 @@ mod in_the_loupe {
|
||||
t.size[0].max(t.size[1])
|
||||
}
|
||||
|
||||
// Given a photo larger than the old 2560 px cap, when I zoom to 1:1, then the loupe holds its pixels
|
||||
// Given a photo larger than the canvas, when I zoom to 1:1, then the whole frame stays about
|
||||
// canvas-sized (a drag redoes that, not 24 MP) and the pixels on screen come from a window
|
||||
// cut from the photo's own pixels
|
||||
#[test]
|
||||
fn one_to_one_is_rendered_at_the_photos_own_size() {
|
||||
fn one_to_one_is_a_canvas_sized_frame_plus_a_native_window() {
|
||||
let (mut h, native) = detail();
|
||||
h.request("engine.execute", json!({"command": "view.zoom100"}), T);
|
||||
h.settle(SETTLE);
|
||||
let want = native.min(crate::state::LOUPE_EDGE_CEILING as usize);
|
||||
let got = rendered_long_edge(&h);
|
||||
assert!(got.abs_diff(want) <= 2, "native {native}: rendered {got}, wanted {want}");
|
||||
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");
|
||||
assert_eq!(region.full.0.max(region.full.1), native, "the window is 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
|
||||
@@ -195,13 +200,12 @@ mod in_the_loupe {
|
||||
h.app.renderer.textures.get(&Slot::Region).map(|t| (t.size[0], t.size[1]))
|
||||
}
|
||||
|
||||
// Given a photo that fits the whole-frame render at 1:1, then no second render is made
|
||||
// Given a fit view, the whole-frame render is all there is: no second render, no original decoded
|
||||
#[test]
|
||||
fn no_window_render_while_the_whole_frame_render_is_sharp_enough() {
|
||||
let (mut h, _) = detail();
|
||||
h.request("engine.execute", json!({"command": "view.zoom100"}), T);
|
||||
h.settle(SETTLE);
|
||||
fn a_fit_view_makes_no_window_render() {
|
||||
let (h, _) = detail();
|
||||
assert_eq!(region_tile(&h), None);
|
||||
assert_eq!(h.app.region_view, None);
|
||||
}
|
||||
|
||||
// Given the user capped the preview at 1600 px, when I zoom to 1:1, then the visible part is
|
||||
@@ -343,4 +347,22 @@ mod in_the_loupe {
|
||||
assert!(main.size[0].max(main.size[1]) >= 700, "still a real render, not a thumbnail: {:?}", main.size);
|
||||
}
|
||||
}
|
||||
|
||||
// Given 400 % on a big photo, when a slider is dragged, then the work per frame follows the
|
||||
// canvas, not the zoom: a draft of the whole frame at canvas scale and a draft of the window
|
||||
#[test]
|
||||
fn a_slider_drag_when_zoomed_in_renders_canvas_sized_drafts() {
|
||||
let (mut h, native) = detail();
|
||||
h.app.ui.zoom = crate::state::Zoom::Percent(400.0);
|
||||
h.settle(SETTLE);
|
||||
h.app.session.begin_interaction("Exposure").unwrap();
|
||||
h.request("engine.execute", json!({"command": "develop.set", "params": {"control": "light.exposure", "value": 0.5}}), T);
|
||||
h.settle(SETTLE);
|
||||
let main = rendered_long_edge(&h);
|
||||
assert!(main <= 1000 && main * 4 < native, "the whole-frame draft is {main} px for a {native} px photo");
|
||||
let region = h.app.region_view.expect("the window is drafted too");
|
||||
assert_eq!(region.full.0.max(region.full.1), native, "at 100 %, magnified by the GPU");
|
||||
let (w, hh) = region_tile(&h).expect("a window draft");
|
||||
assert!(w * hh <= 3_000_000, "{w}×{hh}: the window holds what is on screen, not the frame");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user