diff --git a/crates/ui-egui/src/merge.rs b/crates/ui-egui/src/merge.rs index 61485a271..4d840ae6d 100644 --- a/crates/ui-egui/src/merge.rs +++ b/crates/ui-egui/src/merge.rs @@ -86,6 +86,8 @@ pub struct MergeTask { pub job: MergeJob, /// The dialog options it was started with. pub options: MergeDialog, + /// The photos it merges. + pub ids: Vec, pub progress: Arc>, pub cancel: Arc, rx: Receiver>, @@ -102,8 +104,9 @@ pub struct MergeState { pub preview: Option<(egui::TextureHandle, [usize; 2], MergeDialog, Value)>, pub preview_pixels: Option>, pub error: Option, - /// Options whose preview failed (not retried until they change). - pub failed_options: Option, + /// The photos and options whose preview failed (not retried until either changes or the dialog + /// opens again). + pub failed_options: Option<(Vec, MergeDialog)>, /// The last finished merge (`{id, path, …}`). pub last_result: Option, /// Photos the dialog merges (the selection when it opened). @@ -143,7 +146,7 @@ fn spawn(app: &LightcraftApp, options: &MergeDialog, ids: &[PhotoId], preview: b std::thread::Builder::new().name("photo-merge".into()).spawn(work).map_err(|e| e.to_string())?; #[cfg(target_arch = "wasm32")] work(); - Ok(MergeTask { preview, job, options: options.clone(), progress, cancel, rx, guard: None }) + Ok(MergeTask { preview, job, options: options.clone(), ids: ids.to_vec(), progress, cancel, rx, guard: None }) } /// The options of the last merge of kind `command` started (kept in the library preferences), or @@ -162,6 +165,8 @@ pub fn open(app: &mut LightcraftApp, command: &str) -> Result { } app.merge.ids = ids; app.merge.error = None; + // a preview that failed before is tried again, or the dialog would show neither a preview nor its error (#2708) + app.merge.failed_options = None; app.merge.preview = None; app.merge.preview_pixels = None; if let Some(t) = app.merge.preview_task.take() { @@ -253,7 +258,7 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) { } Err(e) => { app.merge.error = Some(e); - app.merge.failed_options = Some(opts.clone()); + app.merge.failed_options = Some((ids, opts.clone())); } } } @@ -285,7 +290,7 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) { let Some(t) = app.merge.preview_task.take() else { return }; if e != "cancelled" { app.merge.error = Some(e); - app.merge.failed_options = Some(t.options); + app.merge.failed_options = Some((t.ids, t.options)); } } Err(_) => ctx.request_repaint_after(std::time::Duration::from_millis(50)), @@ -362,7 +367,7 @@ fn added_message(what: &str, info: &Value) -> String { } fn last_failed_matches(app: &LightcraftApp, opts: &MergeDialog) -> bool { - app.merge.failed_options.as_ref() == Some(opts) + app.merge.failed_options.as_ref().is_some_and(|(ids, o)| ids == &app.merge.ids && o == opts) } /// The dialog body (options on the left, preview on the right). diff --git a/crates/ui-egui/src/tests_activity.rs b/crates/ui-egui/src/tests_activity.rs index bce9d2eb1..9fa533145 100644 --- a/crates/ui-egui/src/tests_activity.rs +++ b/crates/ui-egui/src/tests_activity.rs @@ -394,6 +394,31 @@ fn merge_dialog_remembers_the_last_settings_per_kind() { let _ = std::fs::remove_dir_all(dir); } +/// Issue #2708: a preview that failed (a photo's file missing) is tried again when the dialog opens again; it used to +/// show neither a preview nor an error until an option changed. +#[test] +fn merge_dialog_retries_a_failed_preview_when_it_opens_again() { + let (mut h, dir) = bracket("merge-retry"); + let (file, away) = (dir.join("IMG_1.dng"), dir.join("IMG_1.away")); + std::fs::rename(&file, &away).unwrap(); + crate::merge::open(&mut h.app, "merge.panorama").unwrap(); + h.step(); + assert!(h.step_until(T, |h| h.app.merge.preview_task.is_none())); + assert!(h.app.merge.error.is_some(), "the preview failed"); + h.app.ui.dialog = None; + h.step(); + // the file is back (a drive reconnected); the same photos and options again + std::fs::rename(&away, &file).unwrap(); + crate::merge::open(&mut h.app, "merge.panorama").unwrap(); + h.step(); + assert!(h.app.merge.preview_task.is_some() || h.app.merge.error.is_some(), "the dialog shows a preview being made, or why not"); + assert!(h.step_until(T, |h| h.app.merge.preview_task.is_none())); + assert!(h.app.merge.preview.is_some(), "{:?}", h.app.merge.error); + h.app.ui.dialog = None; + h.step(); + let _ = std::fs::remove_dir_all(dir); +} + /// HDR for Each Bracket Set runs the merges one after the other, each with its activity row; cancelling one stops the /// rest. #[test] diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 21140ae26..4b038ffbd 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-21 · **Change:** minor (LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (LR-MERGE-HDR / LR-MERGE-PANO: the dialog makes a new preview each time it opens, also after a failed one, #2708; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good @@ -353,7 +353,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | Id | Feature | Tier | Status | Evidence | Missing / notes | |---|---|---|---|---|---| | LR-MERGE-HDR | HDR merge | P2 | 🟡 | `cmd:merge.hdr`, `cmd:dialog.mergeHdr`, `crates/merge/src/frame.rs` (`Frame::lens`), `crates/merge/src/hdr.rs` | auto align (Lanczos-3 warp), deghost None–High + overlay, auto settings, float DNG, Create Stack; the DNG keeps the reference frame's embedded lens corrections (tested by `hdr_merge_keeps_the_reference_frames_lens_corrections`); raws without colour matrices of their own (CR2, NEF, ARW…) keep the colour they open with, written as DNG tags (`starting_colour_tags`; the look's chroma curve has no tag; panoramas too), issue #965; the DNG's `AsShotNeutral` is the median of the inputs' vendor white-balance multipliers, issue #1144. Tested on real Sony A7R VI brackets (67 MP, 2026-10-14): frames agree with the merged radiance within ~1 % in all channels, exposure steps match EXIF within 0.03 EV. A clipped highlight of the reference that moved in other frames (a star at 200 mm) stays instead of becoming a black hole, and deghost (≤ 2048 px maps) catches a brighter frame's ghost in a dark sky (`a_clipped_star_of_the_reference_is_not_erased_by_frames_where_it_moved`, `deghost_removes_a_bright_frames_ghost_in_a_dark_sky`). 🟡 because: AI Denoise doesn't run on merged DNGs (Bayer only; Lightroom's does), JPEG brackets are treated as linear (no camera response curve), and every frame is held as 32-bit float (~0.86 GB per 67 MP frame, so 7 frames need ~7 GB); gap 29 | -| LR-MERGE-PANO | Panorama | P2 | ✅ | `cmd:merge.panorama`, `cmd:dialog.mergePanorama` | spherical/cylindrical/perspective + auto (projected with Lanczos-3), boundary warp, auto crop, fill edges (diffusion), DNG whose `AsShotNeutral` is the median of its frames' as-shot white balance (vendor multipliers for CR2, NEF, ARW…), issue #1144; no lens model / 360° wrap | +| LR-MERGE-PANO | Panorama | P2 | ✅ | `cmd:merge.panorama`, `cmd:dialog.mergePanorama` | spherical/cylindrical/perspective + auto (projected with Lanczos-3), boundary warp, auto crop, fill edges (diffusion), DNG whose `AsShotNeutral` is the median of its frames' as-shot white balance (vendor multipliers for CR2, NEF, ARW…), issue #1144; the dialog makes a new preview each time it opens, also after a failed one (issue #2708, `merge_dialog_retries_a_failed_preview_when_it_opens_again`); no lens model / 360° wrap | | LR-MERGE-HDRPANO | HDR panorama | P2 | ✅ | `cmd:merge.hdrPanorama`, `cmd:dialog.mergeHdrPanorama` | brackets grouped by EXIF | | LR-MERGE-HEADLESS | Merge with last settings | P2 | ✅ | `cmd:merge.hdrLast`, `cmd:merge.panoramaLast`, `cmd:merge.hdrPanoramaLast`, `cmd:merge.hdrBatch`, `crates/catalog/src/stacks.rs` (`bracket_sets`) | no dialog; the options of the last merge of that kind (defaults the first time), kept in the library's prefs.json so they survive a restart; the HDR / Panorama / HDR Panorama dialogs open with them too (`merge_dialog_remembers_the_last_settings_per_kind`). HDR for Each Bracket Set merges a batch like Lightroom's merge of stacks: one HDR per stack among the photos, then per bracket (photos taken less than a second apart, after the shutter time, at exposures ≥ ½ EV apart; bursts at one exposure are skipped), one after the other in the activity stack (cancelling one stops the rest; a failed set doesn't); CLI `merge hdr-batch FILES…` | @@ -798,6 +798,7 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | Date | Change | Summary | |---|---|---| +| 2026-10-23 | patch | LR-MERGE-HDR / LR-MERGE-PANO: the merge dialog makes a new preview each time it opens, also after a failed one (#2708); status unchanged | | 2026-10-21 | patch | LR-IMP-ADD-DIALOG: reconcile #2362 with current main; deleted-album guard and hierarchical picker retained; counts unchanged | | 2026-10-20 | minor | LR-IMP-ADD-DIALOG: #2362 gives the import review the shared hierarchy/search album picker while retaining None/New album and existing destination IDs; status and feature counts unchanged | | 2026-10-21 | patch | KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the slider's row; leaving it (even between two frames) or Tab focus alone does not arm them (issue #1355); status unchanged | diff --git a/docs/whats-new.md b/docs/whats-new.md index cffff83e2..bc0da0188 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -1,5 +1,12 @@ # What's new in LightCraft +## 2026-10-23 + +### Merge previews after a failed one +- The HDR, Panorama and HDR Panorama dialogs make a new preview each time they open (issue #2708). After a preview + had failed once, for example because a photo's file was missing, opening the dialog again with the same options + showed neither a preview nor an error until an option was changed. + ## 2026-10-21 ### Sharper resampling in HDR and panorama merges