mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-14 19:27:01 -04:00
Merge PR #1412
This commit is contained in:
commit
b2a5997e55
5 files changed
+76
-1
No files matched your search
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user