fix: prioritize both cameras during zoom redraws

This commit is contained in:
Gustavo Ziger
2026-10-18 22:41:46 -03:00
parent 1334534e10
commit a94c4e19e6
3 changed files with 41 additions and 6 deletions
+1 -1
View File
@@ -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
+39 -5
View File
@@ -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();
+1
View File
@@ -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())
}