The merge job and its page: a selection to a panorama DNG, confirmed first

dr_ui::merge is the orchestration with no interface in it: decode each
frame to sensor data and build its graph as a session would (orientation,
lens profile); render each through the camera-space tap at proxy size and
detect keypoints there, so the alignment is measured in the undistorted
frame the tiles are rendered in; align; solve one gain per frame from the
proxies' overlaps; draw the aligned set in colour for the page; then wait.
Nothing is written until a Decision arrives (FR-MRG-1). The merge writes
a linear DNG through the outbox with a destination record, so the drain
puts it beside its sources on a folder library and a server alike, and
the library rescans (FR-MRG-3).

merge.slint is the page, on the import page's model: the alignment
table with a failed frame named on its row and the button held off
(FR-MRG-5), the preview, the projection choice, Stop and Back. A
"Merge to panorama" button joins the grid's selection bar at two frames.

Headless, the example produces the fixture's 22 993 x 5 980 DNG in 45 s
on the reference desktop, exposures balanced across the stop of drift.
This commit is contained in:
2026-09-19 15:24:20 +02:00
parent 44ea763c61
commit 2e9a1eb0f0
12 changed files with 1654 additions and 105 deletions
+31
View File
@@ -46,6 +46,8 @@ mod library_ui;
mod live_style;
mod masks_ui;
pub mod memory;
pub mod merge;
mod merge_ui;
mod net_runtime;
mod peaking;
mod place;
@@ -1542,6 +1544,35 @@ pub fn run(paths: Vec<PathBuf>) -> Result<()> {
// through (`dr_plat::imports_supported`).
window.set_import_supported(dr_plat::imports_supported());
// TRACES: FR-MRG-1
{
let merge = merge_ui::MergeController::new(activity.clone());
let library_for_sources = library.clone();
let collections_for_sources = collections.clone();
let library_for_context = library.clone();
let library_for_done = library.clone();
merge_ui::wire(
&window,
merge,
gpu.clone(),
move || library_for_sources.export_sources(&collections_for_sources.selected()),
move || {
let conn = library_for_context.session()?;
Some(merge_ui::Context {
outbox: export::outbox_dir(&conn.account),
conn,
})
},
move |w| {
// Staged beside its sources: upload it now rather than
// on the next sync pass, then look for it, exactly as an
// import does.
drain_outbox(&library_for_done);
w.invoke_library_rescan();
},
);
}
let import = import_ui::ImportController::new(settings.clone(), activity.clone());
let library_for_context = library.clone();
let weak = window.as_weak();