This commit is contained in:
bflatastic committed 2026-10-13 11:00:53 +07:00
commit b2a5997e55
5 files changed
+76 -1

No files matched your search

+1 -1
View File
@@ -64,7 +64,7 @@ impl WireJob {
/// `view`: the slot name, for jobs that keep a stage cache.
pub fn from_job(job: &RenderJob, thumb_cached: bool, view: &str) -> WireJob {
let max_edge = match &job.source {
SourceRef::Demo { max_edge, .. } | SourceRef::File { max_edge, .. } => *max_edge,
SourceRef::Demo { max_edge, .. } | SourceRef::File { max_edge, .. } | SourceRef::Fit { max_edge, .. } => *max_edge,
SourceRef::Loaded(img) => img.image.width.max(img.image.height),
SourceRef::Smart { .. } => 2560,
};
+41
View File
@@ -223,6 +223,13 @@ impl DecodedSource {
#[derive(Clone)]
pub enum SourceRef {
Loaded(Box<DecodedSource>),
/// A larger decoded source of the photo, fitted to `max_edge` by the worker: the thumbnail of
/// the photo open in the loupe is cut from its preview-level source instead of decoding the
/// file again (a second or more for a 64 MP raw).
Fit {
from: Box<DecodedSource>,
max_edge: usize,
},
Demo {
scene: Box<lightcraft_scenes::Scene>,
max_edge: usize,
@@ -251,6 +258,13 @@ impl SourceRef {
pub fn load_source(&self) -> Result<DecodedSource, String> {
let image = match self {
SourceRef::Loaded(a) => return Ok((**a).clone()),
SourceRef::Fit { from, max_edge } => {
if from.image.width.max(from.image.height) <= *max_edge {
return Ok((**from).clone());
}
let image = lightcraft_raster::resample::fit(&from.image, *max_edge, *max_edge, lightcraft_raster::resample::Filter::Box);
return Ok(DecodedSource { image: Arc::new(image), denoised: None, ..(**from).clone() });
}
SourceRef::Demo { scene, max_edge } => crate::demo::scene_pixels(scene, *max_edge)?,
SourceRef::File { path, max_edge, loader, fallback, denoise } => {
let plain = || match loader {
@@ -625,6 +639,15 @@ impl MediaCache {
if cached && let Some(a) = self.get_source(p.id, level) {
return SourceRef::Loaded(Box::new(a));
}
// a thumbnail of a photo whose preview is decoded (the one in the loupe) is cut from it; not
// with a denoised picture, which the thumbnail would lose
if cached
&& level == SourceLevel::Thumb
&& self.denoise.spec(p.id).is_none()
&& let Some(big) = self.get_source(p.id, SourceLevel::Preview)
{
return SourceRef::Fit { from: Box::new(big), max_edge: level.max_edge() };
}
// Procedural scenes have a nominal size: "full" is that size, not unbounded.
let max_edge = level.max_edge().min(match (&p.source, level) {
(Source::Demo { .. }, SourceLevel::Full) => p.width.max(p.height).max(1) as usize,
@@ -1987,6 +2010,24 @@ mod tests {
assert!(mean(&a) > mean(&b) + 10.0, "two stops brighter after: {} vs {}", mean(&a), mean(&b));
}
/// The thumbnail of a photo whose preview-level source is decoded is cut from it: the file (here
/// one nothing can decode) is not read again.
#[test]
fn a_thumbnail_is_cut_from_the_decoded_preview() {
let mut s = crate::Session::new();
let p = Photo::new(PhotoId(1), Source::File { path: "/nowhere/a.ARW".into() }, "a.ARW", "ARW", 2400, 1600, "");
s.catalog.apply(lightcraft_catalog::Op::AddPhoto { photo: Box::new(p.clone()) }).unwrap();
s.media.insert(p.id, SourceLevel::Preview, Arc::new(Rgb32f::from_fn(2400, 1600, |x, _| [x as f32 / 2400.0, 0.5, 0.25])));
let job = s.thumb_job(p.id, 512).unwrap();
assert!(matches!(job.source, SourceRef::Fit { .. }));
let r = job.run();
let thumb = r.rendered.as_ref().unwrap();
assert_eq!((thumb.image.width, thumb.image.height), (512, 341));
s.accept(&r);
let cached = s.media.get(p.id, SourceLevel::Thumb).unwrap();
assert_eq!((cached.width, cached.height), (512, 341), "a thumbnail-sized source is kept, not the preview");
}
#[test]
fn has_source_follows_what_the_session_accepted() {
let mut s = crate::Session::with_demo();
+5
View File
@@ -479,6 +479,11 @@ fn group_header(app: &mut LightcraftApp, ui: &mut egui::Ui, run: &DateRun, ids:
/// [`lightcraft_engine::media::Session::embedded_thumb_is_final`]), which for a folder of raws on a
/// network share is the difference between reading every file whole and reading a preview each.
pub fn request_thumb(app: &mut LightcraftApp, id: PhotoId, size: usize, priority: u32) {
// A slider drag changes the photo every frame: its thumbnail follows once the drag ends, so its
// renders (a raw decode the first time) don't compete with the loupe's drafts.
if app.session.interaction.as_ref().is_some_and(|i| i.photo == id) && app.renderer.thumb(id).is_some() {
return;
}
let bucket = lightcraft_engine::media::thumb_bucket(size);
if let Some(photo) = app.session.catalog.photo(id)
&& app.renderer.thumb_current(photo, bucket, priority)
+20
View File
@@ -1015,6 +1015,26 @@ mod thumbnail_tests {
assert!(work.borrow().jobs.is_empty());
}
#[test]
fn a_slider_drag_leaves_the_edited_photos_thumbnail_until_it_ends() {
let (mut app, work, ctx) = fixture();
request(&mut app);
let (slot, job) = take_job(&work);
finish(&mut app, &work, &ctx, slot, job.run());
app.session.execute("library.select", &serde_json::json!({"ids": [1], "active": 1})).unwrap();
app.session.execute("develop.beginInteraction", &serde_json::json!({"label": "Exposure"})).unwrap();
for v in [0.1, 0.2, 0.3] {
app.session.execute("develop.set", &serde_json::json!({"control": "light.exposure", "value": v})).unwrap();
app.renderer.poll(&ctx, &mut app.session);
request(&mut app);
}
assert_eq!(app.renderer.thumb_jobs_built, 1, "the drag's frames render no thumbnail of the photo");
assert!(work.borrow().jobs.is_empty());
app.session.execute("develop.endInteraction", &serde_json::json!({})).unwrap();
request(&mut app);
assert_eq!(app.renderer.thumb_jobs_built, 2, "the thumbnail follows once the drag ends");
}
#[test]
fn unchanged_queued_frames_build_no_jobs() {
let (mut app, _, _) = fixture();
+9
View File
@@ -1,5 +1,14 @@
# What's new in LightCraft
## 2026-10-13
### Slider drags on large raws
- Dragging a slider no longer re-renders the edited photo's filmstrip thumbnail on every frame; it follows once you
let go. On a 64 MP raw the first of those renders decoded the whole file again, which held up the loupe's
previews for up to a second.
- A thumbnail of the photo open in Detail is made from its already decoded preview instead of decoding the file
again.
## 2026-10-11
### HDR and panorama merges of CR2 keep their colour