mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
LR-VIEW-ZOOM: a pinch holds the loupe's fit size, so it no longer renders at every zoom step
This commit is contained in:
@@ -575,6 +575,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||||||
}
|
}
|
||||||
// a pinch changes the zoom every frame: keep the sizes of before it until it is quiet
|
// a pinch changes the zoom every frame: keep the sizes of before it until it is quiet
|
||||||
let plan = app.size_hold.apply(id, now, navigating, plan);
|
let plan = app.size_hold.apply(id, now, navigating, plan);
|
||||||
|
// the drawn size in pixels, clamped first: a NaN or negative float must not become a size
|
||||||
|
let drawn_px = |points: f32| (points * ppp).round().clamp(0.0, 65_536.0) as usize;
|
||||||
|
let fit_px = app.size_hold.display_fit(now, navigating, [drawn_px(target_rect.width()), drawn_px(target_rect.height())]);
|
||||||
if app.size_hold.holding(now) {
|
if app.size_hold.holding(now) {
|
||||||
ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64(crate::region::HOLD_SECS));
|
ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64(crate::region::HOLD_SECS));
|
||||||
}
|
}
|
||||||
@@ -603,11 +606,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||||||
// at rest the render is shrunk to exactly the size it is drawn at with a Lanczos filter on
|
// at rest the render is shrunk to exactly the size it is drawn at with a Lanczos filter on
|
||||||
// the worker, so the window draws it texel for texel instead of shrinking it with 2-tap
|
// the worker, so the window draws it texel for texel instead of shrinking it with 2-tap
|
||||||
// sampling (the render is up to a 64 px step larger, or the preview level)
|
// sampling (the render is up to a 64 px step larger, or the preview level)
|
||||||
let job = if interacting {
|
let job = if interacting { job } else { job.with_display_fit(fit_px[0], fit_px[1]) };
|
||||||
job
|
|
||||||
} else {
|
|
||||||
job.with_display_fit((target_rect.width() * ppp).round() as usize, (target_rect.height() * ppp).round() as usize)
|
|
||||||
};
|
|
||||||
let job = job.with_overlay(overlay).with_proof(app.ui.soft_proof.then_some(app.ui.proof));
|
let job = job.with_overlay(overlay).with_proof(app.ui.soft_proof.then_some(app.ui.proof));
|
||||||
let job = job.with_hidden_grading(app.ui.grading_hidden_for(id));
|
let job = job.with_hidden_grading(app.ui.grading_hidden_for(id));
|
||||||
app.renderer.request(Slot::Main, job, 100);
|
app.renderer.request(Slot::Main, job, 100);
|
||||||
@@ -626,7 +625,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||||||
let hover_key = match app.hover_preview.clone() {
|
let hover_key = match app.hover_preview.clone() {
|
||||||
Some(h) if !interacting => app.session.preview_job(id, sw.max(8), sh.max(8), !crop_tool, &h.settings).map(|job| {
|
Some(h) if !interacting => app.session.preview_job(id, sw.max(8), sh.max(8), !crop_tool, &h.settings).map(|job| {
|
||||||
// (the same display fit as the After view, drawn texel for texel like it)
|
// (the same display fit as the After view, drawn texel for texel like it)
|
||||||
let job = job.with_display_fit((target_rect.width() * ppp).round() as usize, (target_rect.height() * ppp).round() as usize);
|
let job = job.with_display_fit(fit_px[0], fit_px[1]);
|
||||||
let key = job.key;
|
let key = job.key;
|
||||||
app.renderer.request(Slot::Hover, job, 105);
|
app.renderer.request(Slot::Hover, job, 105);
|
||||||
(key, h.label)
|
(key, h.label)
|
||||||
@@ -666,11 +665,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
|
|||||||
&& let Some(job) = app.session.render_job(id, sw.max(8), sh.max(8), true, !crop_tool)
|
&& let Some(job) = app.session.render_job(id, sw.max(8), sh.max(8), true, !crop_tool)
|
||||||
{
|
{
|
||||||
// (the same display fit as the After view, so both panes are the same texture size)
|
// (the same display fit as the After view, so both panes are the same texture size)
|
||||||
let job = if interacting {
|
let job = if interacting { job } else { job.with_display_fit(fit_px[0], fit_px[1]) };
|
||||||
job
|
|
||||||
} else {
|
|
||||||
job.with_display_fit((target_rect.width() * ppp).round() as usize, (target_rect.height() * ppp).round() as usize)
|
|
||||||
};
|
|
||||||
app.renderer.request(Slot::Before, job, 90);
|
app.renderer.request(Slot::Before, job, 90);
|
||||||
}
|
}
|
||||||
// zoomed past what the whole-frame render holds: also render the part on screen, at no more
|
// zoomed past what the whole-frame render holds: also render the part on screen, at no more
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ pub const HOLD_SECS: f64 = 0.25;
|
|||||||
pub struct SizeHold {
|
pub struct SizeHold {
|
||||||
photo: Option<lightcraft_catalog::PhotoId>,
|
photo: Option<lightcraft_catalog::PhotoId>,
|
||||||
plan: Option<LoupePlan>,
|
plan: Option<LoupePlan>,
|
||||||
|
fit: Option<[usize; 2]>,
|
||||||
until: f64,
|
until: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +340,7 @@ impl SizeHold {
|
|||||||
/// one is running (`gesturing`) and for [`HOLD_SECS`] after. `now` is in seconds.
|
/// one is running (`gesturing`) and for [`HOLD_SECS`] after. `now` is in seconds.
|
||||||
pub fn apply(&mut self, photo: lightcraft_catalog::PhotoId, now: f64, gesturing: bool, fresh: LoupePlan) -> LoupePlan {
|
pub fn apply(&mut self, photo: lightcraft_catalog::PhotoId, now: f64, gesturing: bool, fresh: LoupePlan) -> LoupePlan {
|
||||||
if self.photo != Some(photo) {
|
if self.photo != Some(photo) {
|
||||||
*self = SizeHold { photo: Some(photo), plan: Some(fresh), until: f64::NEG_INFINITY };
|
*self = SizeHold { photo: Some(photo), plan: Some(fresh), fit: None, until: f64::NEG_INFINITY };
|
||||||
return fresh;
|
return fresh;
|
||||||
}
|
}
|
||||||
if gesturing {
|
if gesturing {
|
||||||
@@ -354,6 +355,18 @@ impl SizeHold {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The size the whole frame is shrunk to at rest (the drawn size): the one from before the
|
||||||
|
/// gesture while sizes are held (call after [`Self::apply`] in the same frame), else `fresh`.
|
||||||
|
pub fn display_fit(&mut self, now: f64, gesturing: bool, fresh: [usize; 2]) -> [usize; 2] {
|
||||||
|
match self.fit {
|
||||||
|
Some(held) if gesturing || now < self.until => held,
|
||||||
|
_ => {
|
||||||
|
self.fit = Some(fresh);
|
||||||
|
fresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether sizes are being held at `now` (the caller asks for a repaint when it ends).
|
/// Whether sizes are being held at `now` (the caller asks for a repaint when it ends).
|
||||||
pub fn holding(&self, now: f64) -> bool {
|
pub fn holding(&self, now: f64) -> bool {
|
||||||
now < self.until
|
now < self.until
|
||||||
@@ -889,6 +902,34 @@ mod tests {
|
|||||||
assert_eq!(hold.apply(p, 1.4, false, zoomed), zoomed);
|
assert_eq!(hold.apply(p, 1.4, false, zoomed), zoomed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a pinch, the display-fit size holds like the render sizes and is retaken after it
|
||||||
|
#[test]
|
||||||
|
fn a_pinch_holds_the_display_fit_size() {
|
||||||
|
use lightcraft_catalog::PhotoId;
|
||||||
|
let (fit, zoomed) = (plan(&auto(), sizes(1400.0, 1400.0, 6000)), plan(&auto(), sizes(24000.0, 1400.0, 6000)));
|
||||||
|
let mut hold = SizeHold::default();
|
||||||
|
let p = PhotoId(1);
|
||||||
|
hold.apply(p, 0.0, false, fit);
|
||||||
|
assert_eq!(hold.display_fit(0.0, false, [1000, 800]), [1000, 800]);
|
||||||
|
for k in 1..60 {
|
||||||
|
let now = k as f64 / 60.0;
|
||||||
|
hold.apply(p, now, true, zoomed);
|
||||||
|
assert_eq!(hold.display_fit(now, true, [1000 + k, 800 + k]), [1000, 800], "frame {k}");
|
||||||
|
}
|
||||||
|
// stopped at t = 59/60: still held within the hold time…
|
||||||
|
hold.apply(p, 1.1, false, zoomed);
|
||||||
|
assert_eq!(hold.display_fit(1.1, false, [2560, 2048]), [1000, 800]);
|
||||||
|
// …then the size it ended at is taken, and kept while nothing moves
|
||||||
|
hold.apply(p, 1.3, false, zoomed);
|
||||||
|
assert_eq!(hold.display_fit(1.3, false, [2560, 2048]), [2560, 2048]);
|
||||||
|
assert_eq!(hold.display_fit(1.4, false, [2561, 2049]), [2561, 2049]);
|
||||||
|
// another photo forgets the held size
|
||||||
|
hold.apply(p, 1.5, true, zoomed);
|
||||||
|
hold.display_fit(1.5, true, [2561, 2049]);
|
||||||
|
hold.apply(PhotoId(2), 1.6, true, zoomed);
|
||||||
|
assert_eq!(hold.display_fit(1.6, true, [640, 480]), [640, 480]);
|
||||||
|
}
|
||||||
|
|
||||||
// Given another photo, the held sizes are forgotten
|
// Given another photo, the held sizes are forgotten
|
||||||
#[test]
|
#[test]
|
||||||
fn another_photo_is_planned_afresh() {
|
fn another_photo_is_planned_afresh() {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,11 @@
|
|||||||
# What's new in LightCraft
|
# What's new in LightCraft
|
||||||
|
|
||||||
|
## 2026-10-21
|
||||||
|
|
||||||
|
### A pinch no longer re-renders the photo at every zoom step
|
||||||
|
- Pinching the loupe now keeps the size the fit view is rendered at until the gesture is quiet, as it already did for
|
||||||
|
the render sizes and windows. Before, each frame of a pinch asked for a new full-frame render at a new size.
|
||||||
|
|
||||||
## 2026-10-19
|
## 2026-10-19
|
||||||
|
|
||||||
### Hidden Local locations come back one at a time
|
### Hidden Local locations come back one at a time
|
||||||
|
|||||||
Reference in New Issue
Block a user