Issue #345: photo merge in the activity stack
The final HDR / panorama merge shows a row with its percentage and stage
instead of the progress toast, and stops from its cross ("HDR merge
cancelled"). The activity tests now let the first renders finish before
they run, and the half-second check tolerates a slow frame.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c3bb8e9916
commit
e9e8b5a933
|
|
@ -3352,6 +3352,7 @@ dependencies = [
|
|||
"lightcraft-engine",
|
||||
"lightcraft-faces",
|
||||
"lightcraft-geom",
|
||||
"lightcraft-merge",
|
||||
"lightcraft-meta",
|
||||
"lightcraft-pipeline",
|
||||
"lightcraft-preview",
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ log = { workspace = true }
|
|||
lightcraft-denoise = { workspace = true, features = ["runtime"] }
|
||||
lightcraft-codecs = { workspace = true }
|
||||
lightcraft-faces = { workspace = true }
|
||||
lightcraft-merge = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -227,5 +227,6 @@
|
|||
"{pct} %": "{pct} %",
|
||||
"+{n} more": "+{n} weitere",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1 Aufgabe läuft noch: {names}. Beenden bricht sie ab.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} Aufgaben laufen noch: {names}. Beenden bricht sie ab."
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} Aufgaben laufen noch: {names}. Beenden bricht sie ab.",
|
||||
"{what} merge cancelled": "{what}-Zusammenfügung abgebrochen"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2026,5 +2026,6 @@
|
|||
"Building smart previews": "Smart-Vorschauen werden erstellt",
|
||||
"Discarding smart previews": "Smart-Vorschauen werden verworfen",
|
||||
"Importing Lightroom catalog": "Lightroom-Katalog wird importiert",
|
||||
"Reading Lightroom catalog": "Lightroom-Katalog wird gelesen"
|
||||
"Reading Lightroom catalog": "Lightroom-Katalog wird gelesen",
|
||||
"Merging": "Zusammenfügen"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,5 +227,6 @@
|
|||
"{pct} %": "{pct} %",
|
||||
"+{n} more": "+{n} más",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1 tarea sigue en curso: {names}. Salir la detiene.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} tareas siguen en curso: {names}. Salir las detiene."
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} tareas siguen en curso: {names}. Salir las detiene.",
|
||||
"{what} merge cancelled": "Combinación {what} cancelada"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1871,5 +1871,6 @@
|
|||
"Building smart previews": "Generando vistas previas inteligentes",
|
||||
"Discarding smart previews": "Descartando vistas previas inteligentes",
|
||||
"Importing Lightroom catalog": "Importando catálogo de Lightroom",
|
||||
"Reading Lightroom catalog": "Leyendo catálogo de Lightroom"
|
||||
"Reading Lightroom catalog": "Leyendo catálogo de Lightroom",
|
||||
"Merging": "Combinando"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,5 +227,6 @@
|
|||
"{pct} %": "{pct} %",
|
||||
"+{n} more": "+{n} autres",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1 tâche est encore en cours : {names}. Quitter l'arrête.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} tâches sont encore en cours : {names}. Quitter les arrête."
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} tâches sont encore en cours : {names}. Quitter les arrête.",
|
||||
"{what} merge cancelled": "Fusion {what} annulée"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1871,5 +1871,6 @@
|
|||
"Building smart previews": "Génération des aperçus dynamiques",
|
||||
"Discarding smart previews": "Suppression des aperçus dynamiques",
|
||||
"Importing Lightroom catalog": "Importation du catalogue Lightroom",
|
||||
"Reading Lightroom catalog": "Lecture du catalogue Lightroom"
|
||||
"Reading Lightroom catalog": "Lecture du catalogue Lightroom",
|
||||
"Merging": "Fusion en cours"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,5 +227,6 @@
|
|||
"{pct} %": "{pct}%",
|
||||
"+{n} more": "ほか{n}件",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1件のタスクが実行中です:{names}。終了すると停止します。",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n}件のタスクが実行中です:{names}。終了すると停止します。"
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n}件のタスクが実行中です:{names}。終了すると停止します。",
|
||||
"{what} merge cancelled": "{what}合成をキャンセルしました"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1873,5 +1873,6 @@
|
|||
"Building smart previews": "スマートプレビューを作成中",
|
||||
"Discarding smart previews": "スマートプレビューを破棄中",
|
||||
"Importing Lightroom catalog": "Lightroom カタログを読み込み中",
|
||||
"Reading Lightroom catalog": "Lightroom カタログを読み取り中"
|
||||
"Reading Lightroom catalog": "Lightroom カタログを読み取り中",
|
||||
"Merging": "合成中"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,5 +227,6 @@
|
|||
"{pct} %": "{pct}%",
|
||||
"+{n} more": "+{n} mais",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1 tarefa ainda está em andamento: {names}. Sair a interrompe.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} tarefas ainda estão em andamento: {names}. Sair as interrompe."
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "{n} tarefas ainda estão em andamento: {names}. Sair as interrompe.",
|
||||
"{what} merge cancelled": "{what} mesclagem cancelada"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1871,5 +1871,6 @@
|
|||
"Building smart previews": "Gerando prévias inteligentes",
|
||||
"Discarding smart previews": "Descartando prévias inteligentes",
|
||||
"Importing Lightroom catalog": "Importando catálogo do Lightroom",
|
||||
"Reading Lightroom catalog": "Lendo catálogo do Lightroom"
|
||||
"Reading Lightroom catalog": "Lendo catálogo do Lightroom",
|
||||
"Merging": "Mesclando"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,5 +227,6 @@
|
|||
"{pct} %": "{pct} %",
|
||||
"+{n} more": "ещё {n}",
|
||||
"1 task is still running: {names}. Quitting stops it.": "Ещё выполняется задача: {names}. Выход прервёт её.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "Ещё выполняется задач: {n} — {names}. Выход прервёт их."
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "Ещё выполняется задач: {n} — {names}. Выход прервёт их.",
|
||||
"{what} merge cancelled": "{what}: слияние отменено"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1584,5 +1584,6 @@
|
|||
"Building smart previews": "Создание смарт-просмотров",
|
||||
"Discarding smart previews": "Удаление смарт-просмотров",
|
||||
"Importing Lightroom catalog": "Импорт каталога Lightroom",
|
||||
"Reading Lightroom catalog": "Чтение каталога Lightroom"
|
||||
"Reading Lightroom catalog": "Чтение каталога Lightroom",
|
||||
"Merging": "Слияние"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,5 +227,6 @@
|
|||
"{pct} %": "{pct} %",
|
||||
"+{n} more": "+{n} ще",
|
||||
"1 task is still running: {names}. Quitting stops it.": "1 завдання ще виконується: {names}. Вихід зупинить його.",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "Ще виконуються завдання ({n}): {names}. Вихід зупинить їх."
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "Ще виконуються завдання ({n}): {names}. Вихід зупинить їх.",
|
||||
"{what} merge cancelled": "Об’єднання {what} скасовано"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2065,5 +2065,6 @@
|
|||
"Building smart previews": "Створення смарт-переглядів",
|
||||
"Discarding smart previews": "Видалення смарт-переглядів",
|
||||
"Importing Lightroom catalog": "Імпорт каталогу Lightroom",
|
||||
"Reading Lightroom catalog": "Читання каталогу Lightroom"
|
||||
"Reading Lightroom catalog": "Читання каталогу Lightroom",
|
||||
"Merging": "Об’єднання"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,5 +227,6 @@
|
|||
"{pct} %": "{pct}%",
|
||||
"+{n} more": "还有 {n} 项",
|
||||
"1 task is still running: {names}. Quitting stops it.": "仍有 1 个任务在运行:{names}。退出将停止它。",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "仍有 {n} 个任务在运行:{names}。退出将停止它们。"
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "仍有 {n} 个任务在运行:{names}。退出将停止它们。",
|
||||
"{what} merge cancelled": "已取消 {what} 合并"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1871,5 +1871,6 @@
|
|||
"Building smart previews": "正在构建智能预览",
|
||||
"Discarding smart previews": "正在丢弃智能预览",
|
||||
"Importing Lightroom catalog": "正在导入 Lightroom 目录",
|
||||
"Reading Lightroom catalog": "正在读取 Lightroom 目录"
|
||||
"Reading Lightroom catalog": "正在读取 Lightroom 目录",
|
||||
"Merging": "正在合并"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -227,5 +227,6 @@
|
|||
"{pct} %": "{pct}%",
|
||||
"+{n} more": "還有 {n} 項",
|
||||
"1 task is still running: {names}. Quitting stops it.": "仍有 1 個工作在執行:{names}。結束將停止它。",
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "仍有 {n} 個工作在執行:{names}。結束將停止它們。"
|
||||
"{n} tasks are still running: {names}. Quitting stops them.": "仍有 {n} 個工作在執行:{names}。結束將停止它們。",
|
||||
"{what} merge cancelled": "已取消{what}合併"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1871,5 +1871,6 @@
|
|||
"Building smart previews": "正在建立智慧型預覽",
|
||||
"Discarding smart previews": "正在捨棄智慧型預覽",
|
||||
"Importing Lightroom catalog": "正在匯入 Lightroom 目錄",
|
||||
"Reading Lightroom catalog": "正在讀取 Lightroom 目錄"
|
||||
"Reading Lightroom catalog": "正在讀取 Lightroom 目錄",
|
||||
"Merging": "正在合併"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -468,7 +468,8 @@ mod tests {
|
|||
fn demo() -> crate::headless::Headless {
|
||||
let app = crate::LightcraftApp::new(lightcraft_engine::Session::with_demo(), crate::Services { png: None, ..Default::default() });
|
||||
let mut h = crate::headless::Headless::new(app, [1200.0, 800.0], 1.0);
|
||||
h.step();
|
||||
// no render left running at exit (see tests_activity::demo)
|
||||
h.settle(std::time::Duration::from_secs(20));
|
||||
h
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! Previews and the final merge run [`lightcraft_engine::merge::MergeJob`]s on worker threads with
|
||||
//! progress and cancellation; the window stays responsive. Changing an option cancels the running
|
||||
//! preview and starts a new one. The final merge keeps running after the dialog closes (progress
|
||||
//! in a toast); when it finishes, the result is written, imported and selected
|
||||
//! in the activity stack); when it finishes, the result is written, imported and selected
|
||||
//! ([`lightcraft_engine::Session::finish_merge`]).
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
|
@ -12,6 +12,7 @@ use std::sync::mpsc::{Receiver, channel};
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use lightcraft_catalog::PhotoId;
|
||||
use lightcraft_engine::activity::{Cancel, TaskGuard, Unit};
|
||||
use lightcraft_engine::merge::{MergeJob, MergeOutput};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
|
@ -88,6 +89,8 @@ pub struct MergeTask {
|
|||
pub progress: Arc<Mutex<(f32, String)>>,
|
||||
pub cancel: Arc<AtomicBool>,
|
||||
rx: Receiver<Result<MergeOutput, String>>,
|
||||
/// The final merge's row in the activity stack (previews have none).
|
||||
guard: Option<TaskGuard>,
|
||||
}
|
||||
|
||||
/// Merge state of the app.
|
||||
|
|
@ -140,7 +143,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 })
|
||||
Ok(MergeTask { preview, job, options: options.clone(), progress, cancel, rx, guard: None })
|
||||
}
|
||||
|
||||
/// Open the merge dialog for the selection.
|
||||
|
|
@ -169,7 +172,11 @@ pub fn start_final(app: &mut LightcraftApp, opts: &MergeDialog) -> Result<Value,
|
|||
t.cancel.store(true, Ordering::Relaxed);
|
||||
}
|
||||
let ids = app.merge.ids.clone();
|
||||
let task = spawn(app, opts, &ids, false)?;
|
||||
let mut task = spawn(app, opts, &ids, false)?;
|
||||
let guard = app.session.activity.start("merge", "Merging", Cancel::Flag(task.cancel.clone()));
|
||||
guard.set_unit(Unit::Percent);
|
||||
guard.progress(0, 1000);
|
||||
task.guard = Some(guard);
|
||||
app.merge.final_task = Some(task);
|
||||
app.merge.last.insert(opts.command.clone(), opts.clone());
|
||||
Ok(json!({"started": true, "photos": ids.len()}))
|
||||
|
|
@ -265,6 +272,7 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
app.merge.last_result = Some(v);
|
||||
app.toast(ctx, crate::i18n::tr_format!("{what} merge added", what = what));
|
||||
}
|
||||
Err(_) if t.cancel.load(Ordering::Relaxed) => app.toast(ctx, crate::i18n::tr_format!("{what} merge cancelled", what = what)),
|
||||
Err(e) => {
|
||||
app.ui.status = e.to_string();
|
||||
app.toast(ctx, crate::i18n::tr_format!("{what} merge failed: {e}", e = e, what = what));
|
||||
|
|
@ -273,8 +281,11 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
Err(_) => {
|
||||
let (f, stage) = t.progress.lock().map(|g| g.clone()).unwrap_or_default();
|
||||
let now = ctx.input(|i| i.time);
|
||||
app.ui.toast = Some((crate::i18n::tr_format!("Merging… {stage} {:.0}%", f * 100.0, stage = stage), now + 0.5, None));
|
||||
if let Some(guard) = &t.guard {
|
||||
let f = if f.is_finite() { f.clamp(0.0, 1.0) } else { 0.0 };
|
||||
guard.progress((f * 1000.0) as u64, 1000);
|
||||
guard.detail(crate::i18n::tr(&stage));
|
||||
}
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ const T: Duration = Duration::from_secs(20);
|
|||
fn demo() -> Headless {
|
||||
let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() });
|
||||
let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
|
||||
h.step();
|
||||
// let the first renders finish: a render still running when the test binary exits can crash in the
|
||||
// GPU driver's teardown (the preview pool doesn't join its workers)
|
||||
h.settle(T);
|
||||
h
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +55,10 @@ fn rows_appear_after_half_a_second_and_cancel_by_click() {
|
|||
let g = h.app.session.activity.start("export", "Exporting", Cancel::Yes);
|
||||
g.progress(2, 5);
|
||||
h.step();
|
||||
assert!(!has(&h, &format!("activity:row:{}", g.id())), "not before 0.5 s");
|
||||
// a frame on a loaded machine can outlast the half second: only judge a task still young after it
|
||||
if h.app.session.activity.list()[0].age_ms < crate::panels::activity::SHOW_AFTER_MS {
|
||||
assert!(!has(&h, &format!("activity:row:{}", g.id())), "not before 0.5 s");
|
||||
}
|
||||
wait_visible(&mut h);
|
||||
assert!(has(&h, &format!("activity:row:{}", g.id())));
|
||||
click(&mut h, &format!("activity:cancel:{}", g.id()));
|
||||
|
|
@ -242,7 +247,7 @@ fn slow_import(tag: &str, n: usize) -> (Headless, std::path::PathBuf) {
|
|||
})
|
||||
}));
|
||||
let mut h = Headless::new(LightcraftApp::new(s, Services { png: None, ..Default::default() }), [1200.0, 800.0], 1.0);
|
||||
h.step();
|
||||
h.settle(T);
|
||||
(h, dir)
|
||||
}
|
||||
|
||||
|
|
@ -282,3 +287,45 @@ fn scan_shows_a_row_and_cancel_closes_it() {
|
|||
assert!(h.app.ui.dialog.is_none(), "no review opened");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A library of three bracketed DNGs (−2, 0, +2 EV) on disk, all selected.
|
||||
fn bracket(tag: &str) -> (Headless, std::path::PathBuf) {
|
||||
let dir = std::env::temp_dir().join(format!("lc-activity-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut paths = Vec::new();
|
||||
for (i, bytes) in lightcraft_merge::synth::bracket_dngs(1600, 1200, &[-2.0, 0.0, 2.0]).unwrap().into_iter().enumerate() {
|
||||
let p = dir.join(format!("IMG_{i}.dng"));
|
||||
std::fs::write(&p, bytes).unwrap();
|
||||
paths.push(p.to_string_lossy().to_string());
|
||||
}
|
||||
let mut session = lightcraft_engine::Session::new().with_fs();
|
||||
let r = session.execute("library.import", &json!({"paths": paths})).unwrap();
|
||||
let ids: Vec<u64> = r["imported"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
||||
session.execute("library.select", &json!({"ids": ids})).unwrap();
|
||||
let app = LightcraftApp::new(session, Services { png: None, ..Default::default() });
|
||||
let mut h = Headless::new(app, [1200.0, 800.0], 1.0);
|
||||
h.settle(T);
|
||||
(h, dir)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_merge_shows_a_row_and_cancels() {
|
||||
let (mut h, dir) = bracket("merge");
|
||||
let before = h.app.session.catalog.len();
|
||||
crate::merge::start_last(&mut h.app, "merge.hdr").unwrap();
|
||||
h.step();
|
||||
let rows = h.app.session.activity.list();
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!((rows[0].kind, rows[0].label.as_str(), rows[0].unit), ("merge", "Merging", Unit::Percent));
|
||||
assert!(rows[0].cancellable);
|
||||
assert_eq!(rows[0].total, 1000);
|
||||
h.app.session.activity.cancel(rows[0].id).unwrap();
|
||||
assert!(h.step_until(T, |h| h.app.merge.final_task.is_none()));
|
||||
assert!(h.app.merge.last_result.is_none(), "nothing merged");
|
||||
assert_eq!(h.app.session.catalog.len(), before, "no photo added");
|
||||
assert!(h.app.session.activity.list().is_empty());
|
||||
let toast = h.app.ui.toast.as_ref().map(|t| t.0.clone()).unwrap_or_default();
|
||||
assert_eq!(toast, "HDR merge cancelled");
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue