From a94c4e19e673db6abb1d2449ecc25ca88eda5aba Mon Sep 17 00:00:00 2001 From: Gustavo Ziger Date: Sun, 18 Oct 2026 22:41:46 -0300 Subject: [PATCH] fix: prioritize both cameras during zoom redraws --- crates/automation/src/server.rs | 2 +- crates/ui-egui/src/canvas.rs | 44 +++++++++++++++++++++--- crates/ui-egui/src/profile_open_tests.rs | 1 + 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/crates/automation/src/server.rs b/crates/automation/src/server.rs index 97e622dbde..f52376a9c5 100644 --- a/crates/automation/src/server.rs +++ b/crates/automation/src/server.rs @@ -308,7 +308,7 @@ pub struct MenuParams { pub struct UiSetParams { /// Fields for the control method `ui.set`: tool, panels, dock, dockTabs, dockWidth, colorPanel /// ({background: bool} picks which swatch the Color panel edits), maskTarget, - /// vectorMaskTarget, selectionMode, layerFilter (by kind, name or blend mode), zoom, center, rotation (view angle in degrees), fit, theme (pro, proMedium, studio, + /// vectorMaskTarget, selectionMode, zoom, center, rotation (view angle in degrees), fit, theme (pro, proMedium, studio, /// studioLight, classic, solarizedDark, adwaita, adwaitaDark, breezeLight, breezeDark, softLight, softDark), brushSection, brushTab, /// brushesView, brushPicker ([x, y] opens the Brush Preset picker there, null closes it), /// brushPickerName, brushPickerStroke, brushPickerTip (the picker card's parts, at least one diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 8474d05943..f8034eb195 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2052,6 +2052,12 @@ fn visible_doc_rect(xf: &ViewXform) -> DRect { DRect::new(c(x0.floor()) - 2, c(y0.floor()) - 2, c(x1.ceil()) + 2, c(y1.ceil()) + 2) } +/// Prioritize deferred redraw blocks visible at either the target or the animated display camera. +/// The displayed camera may still show old texture contents outside the target's visible area. +fn redraw_focus(target: &ViewXform, displayed: &ViewXform) -> DRect { + visible_doc_rect(target).union(&visible_doc_rect(displayed)) +} + /// Zoomed-out Image › Adjustments preview on a large document: the wgpu compositor renders the /// reduced preview document (`adjust_preview::gpu_proxy`) into its own texture, over the target's /// area after the first frame. Returns (factor, gpu key, dimensions) to draw. @@ -3134,7 +3140,6 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect: // Document rendering follows the target; only shader uniforms and overlay geometry ease. let target_xf = ViewXform { rect, zoom: view.zoom / ppp, center: view.center, flip, rotation: view.rotation, aspect }; let render_zoom = view.zoom; - let render_visible = visible_doc_rect(&target_xf); let mut display_view = animated_view(&mut view, target_xf, ppp, app.session.prefs().general.animated_zoom, ctx.input(|i| i.time)); if primary && let Some(stored) = app.ui.views.get_mut(idx) { stored.zoom_animating = view.zoom_animating; @@ -3213,7 +3218,7 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect: offset: [0.0, 0.0], }; paint_gpu_view(&painter, rect, params, &pattern); - } else if gpu_ok && ensure_gpu(app, &ctx, idx, render_visible) { + } else if gpu_ok && ensure_gpu(app, &ctx, idx, redraw_focus(&target_xf, &xf)) { on_gpu = true; app.perf.gpu = true; // Shadow, checkerboard, document and pixel grid in one custom shader (gpu_canvas.rs). @@ -4343,9 +4348,6 @@ pub fn canvas_view(app: &mut PhotocraftApp, ui: &mut egui::Ui, idx: usize, rect: view } -/// Zoom to `new_zoom` about the pointer `p`. By default the document point under it stays under -/// it; with Preferences › Tools › Zoom Clicked Point to Center (`center_on_point`) it moves to the -/// centre of the view instead (#204). /// Where a wheel or pinch zoom anchors for a pointer at `p`: the pointer itself over the image, /// otherwise the nearest point of the image (#3720). Anchoring on a pointer out on the gray /// pasteboard slid the image away from it, out of the window after a few notches; Photoshop keeps @@ -4360,6 +4362,8 @@ fn wheel_anchor(xf: &ViewXform, p: Pos2, doc_size: [u32; 2]) -> Pos2 { if c == d { p } else { xf.to_screen(c[0] as f32, c[1] as f32) } } +/// Zoom to `new_zoom` about `p`: keep its document point under the pointer, or move it to the +/// view centre when Zoom Clicked Point to Center (`center_on_point`) is enabled. fn zoom_about(view: &mut View, xf: &ViewXform, p: Pos2, new_zoom: f32, center_on_point: bool, ppp: f32) { let before = xf.to_doc(p); view.zoom = new_zoom; @@ -8952,14 +8956,44 @@ mod rotation_preview_isolation_tests { mod zoom_animation_tests { use super::*; + /// Screen geometry shared by the zoom-transition regressions. fn camera(view: &View) -> ViewXform { ViewXform { rect: Rect::from_min_size(Pos2::ZERO, vec2(1000.0, 700.0)), zoom: view.zoom, center: view.center, flip: false, rotation: 0.0, aspect: 1.0 } } + /// A settled camera whose transition state starts empty. fn view() -> View { View { zoom: 1.0, center: [200.0, 150.0], doc_size: [400, 300], fit_pending: false, ..Default::default() } } + /// Deferred uploads prioritize both visible areas throughout a recentering zoom. + #[test] + fn deferred_redraw_focus_covers_target_and_displayed_cameras() { + for rotation in [0.0, 37.0] { + let mut v = view(); + let mut initial = camera(&v); + initial.rotation = rotation; + animated_view(&mut v, initial, 1.0, true, 0.0); + v.zoom = 2.0; + v.center = [1600.0, 900.0]; + v.request_zoom_animation(true); + let target = ViewXform { zoom: v.zoom, center: v.center, ..initial }; + let target_visible = visible_doc_rect(&target); + for time in [0.0, 0.11, 0.22] { + let shown = animated_view(&mut v, target, 1.0, true, time); + let displayed = ViewXform { zoom: shown.zoom, center: shown.center, ..target }; + let displayed_visible = visible_doc_rect(&displayed); + let focus = redraw_focus(&target, &displayed); + assert_eq!(focus.intersect(&target_visible), target_visible); + assert_eq!(focus.intersect(&displayed_visible), displayed_visible); + if time == 0.0 { + assert_ne!(focus, target_visible, "the old view must also receive priority"); + } + } + assert_eq!(redraw_focus(&target, &target), target_visible, "settled views keep the same focus"); + } + } + #[test] fn disabling_animated_zoom_snaps_a_secondary_canvas_on_its_next_frame() { let ctx = egui::Context::default(); diff --git a/crates/ui-egui/src/profile_open_tests.rs b/crates/ui-egui/src/profile_open_tests.rs index b071048dea..83523da0d1 100644 --- a/crates/ui-egui/src/profile_open_tests.rs +++ b/crates/ui-egui/src/profile_open_tests.rs @@ -13,6 +13,7 @@ use photocraft_doc::{Color, ColorMode, Document, SampleType, Size}; use photocraft_engine::Session; use serde_json::Value; +/// The embedded-profile choice uses the current built-in profile's displayed name. fn use_embedded() -> String { format!("Use the embedded profile ({})", photocraft_cms::Builtin::AdobeRgbCompat.description()) }