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
+54 -6
View File
@@ -18,10 +18,11 @@ import { PresetSheet, ScopeChips, ScopeKind } from "presets.slint";
import { FocusMarks, FocusPanel, Peaking } from "peaking.slint";
import { SettingsPage } from "settings.slint";
import { ImportPage } from "import.slint";
import { MergePage, MergeFrameRow } from "merge.slint";
import { Capture, StatusBar, InfoPanel } from "develop.slint";
import { ToolRail } from "toolrail.slint";
export { LibraryCell, TimelineBar, CollectionRow, ActivityRow, HistogramView, PersonChip }
export { LibraryCell, TimelineBar, CollectionRow, ActivityRow, HistogramView, PersonChip, MergeFrameRow }
export { GestureRow }
export { ViewMode, GradientHandle, HandleRole, SpotHandle, SpotRole }
@@ -239,7 +240,7 @@ export component AppWindow inherits Window {
/// So the guard has to be this, and the `if` has to be spelled from it, or
/// the two can drift apart again.
out property <bool> library-visible:
!root.show-import && !root.show-settings && !root.show-identity
!root.show-import && !root.show-merge && !root.show-settings && !root.show-identity
&& !root.show-launch && root.show-library;
in-out property <[LibraryCell]> library-cells;
in property <int> library-total: 0;
@@ -821,6 +822,30 @@ export component AppWindow inherits Window {
in property <string> import-error: "";
in property <bool> import-can-start: false;
// TRACES: FR-MRG-1 | FR-MRG-7
// The merge page, on the import page's model: a decision to be
// committed or abandoned, with a running state, over the library.
in-out property <bool> show-merge: false;
in property <string> merge-stage: "";
in property <bool> merge-running: false;
in property <float> merge-fraction: 0.0;
in property <string> merge-status: "";
in property <string> merge-error: "";
in property <bool> merge-aligned: false;
in property <bool> merge-complete: false;
in property <string> merge-summary: "";
in property <[MergeFrameRow]> merge-frames;
in property <image> merge-preview;
in property <int> merge-projection-selected: 0;
in property <bool> merge-done: false;
/// Merge the grid's selection: opens the page and starts the alignment.
callback library-merge-selection();
/// The confirmation (FR-MRG-1). Nothing is written before this.
callback merge-confirm();
callback merge-abandon();
callback merge-projection-picked(int);
callback merge-close();
callback import-open();
callback import-close();
callback import-volume-picked(int);
@@ -1270,7 +1295,28 @@ in property <bool> panel-visible: true;
close() => { root.import-close(); }
}
if !root.show-import && root.show-settings: SettingsPage {
if !root.show-import && root.show-merge: MergePage {
width: 100%;
height: 100%;
stage: root.merge-stage;
running: root.merge-running;
fraction: root.merge-fraction;
status: root.merge-status;
error: root.merge-error;
aligned: root.merge-aligned;
complete: root.merge-complete;
summary: root.merge-summary;
frames: root.merge-frames;
preview: root.merge-preview;
projection-selected: root.merge-projection-selected;
done: root.merge-done;
merge() => { root.merge-confirm(); }
abandon() => { root.merge-abandon(); }
projection-picked(i) => { root.merge-projection-picked(i); }
close() => { root.merge-close(); }
}
if !root.show-import && !root.show-merge && root.show-settings: SettingsPage {
width: 100%;
height: 100%;
@@ -1404,7 +1450,7 @@ in property <bool> panel-visible: true;
// Like the launch screen, this replaces the window rather than
// overlaying it — see `show-identity`.
if !root.show-import && !root.show-settings && root.show-identity: IdentityScreen {
if !root.show-import && !root.show-merge && !root.show-settings && root.show-identity: IdentityScreen {
width: 100%;
height: 100%;
people: root.identity-people;
@@ -1462,7 +1508,7 @@ in property <bool> panel-visible: true;
// The launch screen replaces the whole window rather than overlaying it:
// there is no library to look at until an account is configured.
if !root.show-import && !root.show-settings && !root.show-identity && root.show-launch: LaunchScreen {
if !root.show-import && !root.show-merge && !root.show-settings && !root.show-identity && root.show-launch: LaunchScreen {
width: 100%;
height: 100%;
signed-in: root.launch-signed-in;
@@ -1638,6 +1684,8 @@ in property <bool> panel-visible: true;
export-to-server: root.export-to-server;
export-selection => { root.library-export-selection(); }
cancel-export => { root.library-cancel-export(); }
// TRACES: FR-MRG-1
merge-selection => { root.library-merge-selection(); }
sweep-done: root.library-sweep-done;
sweep-total: root.library-sweep-total;
@@ -1753,7 +1801,7 @@ in property <bool> panel-visible: true;
}
}
if !root.show-import && !root.show-settings && !root.show-identity && !root.show-launch && !root.show-library: VerticalLayout {
if !root.show-import && !root.show-merge && !root.show-settings && !root.show-identity && !root.show-launch && !root.show-library: VerticalLayout {
width: 100%;
height: 100%;