Files
lightcraft/crates/engine/src/tests_merge.rs
T
Brandon ThomasandClaude Opus 5.5 ce3dd5e156 Merge main (Photo Merge); M1.14b: stack merge results with their sources
Photo Merge's Create Stack option now puts the merged DNG on top of a
collapsed stack of its sources (Catalog::stack_with_ops), so the grid
shows the result; test in tests_merge (and no stack without the option).
Photo Merge dialogs live in a Photo > Photo Merge submenu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:00:51 -04:00

91 lines
4.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Photo Merge through the command layer: synthetic brackets / panorama views on disk → merged
//! DNG next to the sources → imported, selected, Auto Settings / auto crop applied.
use serde_json::json;
use crate::Session;
fn temp_dir(tag: &str) -> std::path::PathBuf {
let d = std::env::temp_dir().join(format!("lc-merge-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
fn import(s: &mut Session, paths: Vec<String>) -> Vec<u64> {
let r = s.execute("library.import", &json!({"paths": paths})).unwrap();
r["imported"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect()
}
#[test]
fn hdr_merge_command_creates_and_imports_a_dng() {
let dir = temp_dir("hdr");
let mut paths = Vec::new();
for (i, b) in lightcraft_merge::synth::bracket_dngs(480, 320, &[-2.0, 0.0, 2.0]).into_iter().enumerate() {
let p = dir.join(format!("IMG_{i}.dng"));
std::fs::write(&p, b).unwrap();
paths.push(p.to_string_lossy().to_string());
}
let mut s = Session::new().with_fs();
let ids = import(&mut s, paths);
assert_eq!(ids.len(), 3);
// preview: rendered PNG, nothing added
let png = dir.join("preview.png");
let pv = s.execute("merge.hdr", &json!({"ids": ids, "deghost": "medium", "preview": true, "showOverlay": true, "previewPath": png})).unwrap();
assert!(png.exists() && pv["previewSize"][0].as_u64().unwrap() > 100, "{pv}");
assert_eq!(s.catalog.photos().count(), 3);
let ev: Vec<f64> = pv["ev"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
assert!((ev[0] + 2.0).abs() < 0.1 && (ev[2] - 2.0).abs() < 0.1, "{ev:?}");
// full merge
let r = s.execute("merge.hdr", &json!({"ids": ids, "autoSettings": true, "stack": true})).unwrap();
let path = r["path"].as_str().unwrap();
assert!(path.ends_with("IMG_0-HDR.dng"), "{path}");
let id = lightcraft_catalog::PhotoId(r["id"].as_u64().unwrap());
let p = s.catalog.photo(id).unwrap().clone();
assert_eq!((p.width, p.height), (480, 320));
assert_eq!(p.kind, lightcraft_catalog::MediaKind::Raw);
assert_eq!(s.selection.active, Some(id));
let d = s.develop_of(id).unwrap();
assert!(d.light.exposure != 0.0 || d.light.highlights != 0.0, "auto settings applied");
let img = s.render_now(id, 256, 256).unwrap();
assert_eq!(img.image.width, 256);
// Create Stack: the result tops a collapsed stack of its sources, so the grid shows the result
let st = s.catalog.stack_of(id).expect("merge result stacked").clone();
assert_eq!(st.top(), id);
assert_eq!(st.photos[1..].iter().map(|p| p.0).collect::<Vec<_>>(), ids);
assert!(st.collapsed);
assert_eq!(s.visible_cloned(), vec![id]);
// the second merge doesn't overwrite the first
let r2 = s.execute("merge.hdr", &json!({"ids": ids, "autoSettings": false})).unwrap();
assert!(r2["path"].as_str().unwrap().ends_with("IMG_0-HDR-2.dng"));
let id2 = lightcraft_catalog::PhotoId(r2["id"].as_u64().unwrap());
assert!(s.catalog.stack_of(id2).is_none(), "no stack without the option");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn panorama_command_with_auto_crop() {
let dir = temp_dir("pano");
let mut paths = Vec::new();
for (i, v) in lightcraft_merge::synth::pano_views(480, 360, 420.0, &[-25.0, 0.0, 25.0]).into_iter().enumerate() {
let img = v.to_srgb8();
let png = lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(&img), &lightcraft_codecs::EncodeMeta::default()).unwrap();
let p = dir.join(format!("P_{i}.png"));
std::fs::write(&p, png).unwrap();
paths.push(p.to_string_lossy().to_string());
}
let mut s = Session::new().with_fs();
let ids = import(&mut s, paths);
let r = s.execute("merge.panorama", &json!({"ids": ids, "projection": "cylindrical", "autoCrop": true, "autoSettings": false})).unwrap();
assert_eq!(r["projection"], "cylindrical");
assert_eq!(r["used"].as_array().unwrap().len(), 3);
let id = lightcraft_catalog::PhotoId(r["id"].as_u64().unwrap());
let p = s.catalog.photo(id).unwrap().clone();
assert!(p.width > 800 && p.height > 300, "{}×{}", p.width, p.height);
let crop = s.develop_of(id).unwrap().crop.geometry.rect;
assert!(crop.width() < 1.0 && crop.width() * crop.height() > 0.5, "{crop:?}");
// too few photos
assert!(s.execute("merge.panorama", &json!({"ids": [ids[0]]})).is_err());
let _ = std::fs::remove_dir_all(&dir);
}