Files
DarkRoom/ui/dr-ui/ui/merge.slint
T

307 lines
14 KiB
Plaintext
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.
// TRACES: FR-MRG-1 | FR-MRG-4 | FR-MRG-5 | FR-MRG-7
// The merge page: a panorama proposed, then confirmed.
//
// Same shape as the import page — a header with the one verb, a progress
// bar under it while work runs, a scrolling body — because that is the
// shape a page that runs a job has in this application, and the merge is
// one. The difference is the pause in the middle: the job aligns, this
// page shows what it found, and nothing is written until the photographer
// presses Merge (FR-MRG-1). A frame that could not be placed is named on
// its row and the button stays off (FR-MRG-5).
import { Theme } from "theme.slint";
import { Button, PanelHeading, Label, Value, Caption, Panel, ProgressBar } from "widgets.slint";
import { Segmented, SliderRow } from "controls.slint";
/// One source frame's line in the alignment table.
export struct MergeFrameRow {
name: string,
/// "yaw −55.9° · pitch −1.6°", or why it is not aligned.
detail: string,
aligned: bool,
}
export component MergePage inherits Rectangle {
/// The stage the job is in, as a word for the header: "Reading",
/// "Aligning", "Merging"; empty when it is waiting or done.
in property <string> stage;
in property <bool> running: false;
in property <float> fraction: 0;
/// One line of what happened last, or what is happening.
in property <string> status;
in property <string> error;
/// The alignment, once there is one.
in property <bool> aligned: false;
/// Whether every frame was placed — the merge can only go ahead then.
in property <bool> complete: false;
in property <string> summary;
in property <[MergeFrameRow]> frames;
/// The preview of the aligned set, drawn by Rust from the proxies.
in property <image> preview;
in property <[string]> projection-labels: ["Auto", "Perspective", "Cylindrical", "Spherical"];
in property <int> projection-selected: 0;
// TRACES: FR-MRG-4
// The border: cropped to the picture, or filled with invented pixels.
// Crop is the default and the honest one; the fill is offered only
// where the device has the model, and what it invents is on the
// preview before it is confirmed.
in property <[string]> border-labels: ["Crop to the picture", "Fill the border"];
in property <int> border-selected: 0;
/// Whether the fill can be offered at all, and where it would run —
/// "TensorRT", "CPU" — or why not.
in property <bool> fill-available: false;
in property <string> fill-note;
/// Whether the preview on screen has its border filled.
in property <bool> preview-filled: false;
/// The fill's knobs — experimental, so every one is on the page: the
/// working scale (the composite's resolution over this), the coverage
/// fringe eroded, the coarse pass's reduction, the fine band width,
/// how deep the mirrored context reaches, and the feather across the
/// seam. Working pixels, at the merge's working scale.
in property <float> fill-scale: 2;
in property <float> fill-erosion: 4;
in property <float> fill-coarse: 1;
in property <float> fill-band: 192;
in property <float> fill-mirror: 0;
in property <float> fill-feather: 48;
/// The composite, once written: its name for the status line, and
/// whether Open can show it.
in property <bool> done: false;
callback merge();
callback abandon();
callback projection-picked(int);
callback border-picked(int);
/// A knob turned: its name ("scale", "erosion", "coarse", "band",
/// "mirror", "feather") and the new value.
callback fill-knob(string, float);
callback close();
background: Theme.ground;
FocusScope {
width: 0px;
height: 0px;
init => { self.focus(); }
}
VerticalLayout {
Rectangle {
height: 44px;
background: Theme.surface;
HorizontalLayout {
padding-left: Theme.gap;
padding-right: Theme.gap;
spacing: Theme.gap;
Button {
text: "‹ Back";
y: (parent.height - self.height) / 2;
clicked => { root.close(); }
}
Value { text: "Panorama"; }
Rectangle { horizontal-stretch: 1; }
if root.running: Button {
text: "Stop";
y: (parent.height - self.height) / 2;
clicked => { root.abandon(); }
}
// The one verb, and it is off until there is a complete
// alignment to confirm: pressing it is the confirmation.
if !root.running && !root.done: Button {
text: "Merge";
primary: true;
y: (parent.height - self.height) / 2;
enabled: root.aligned && root.complete;
clicked => { root.merge(); }
}
}
Rectangle {
y: parent.height - 1px;
height: 1px;
background: Theme.rule;
}
}
if root.running: ProgressBar {
fraction: root.fraction;
indeterminate: root.stage == "Aligning";
label: root.stage;
}
if root.error != "": Rectangle {
height: 32px;
background: Theme.surface;
HorizontalLayout {
padding-left: Theme.gap;
padding-right: Theme.gap;
Caption { text: root.error; warn: true; overflow: elide; }
}
}
Flickable {
vertical-stretch: 1;
viewport-height: content.preferred-height;
content := VerticalLayout {
width: 100%;
padding: Theme.gap;
spacing: Theme.gap;
alignment: start;
// --- the proposal --------------------------------------
if root.aligned: Panel {
PanelHeading { text: "Alignment"; }
// The set as it will lie on the chosen surface. Drawn at
// proxy resolution, which is the resolution the alignment
// was found at: ghosting here is the alignment's error,
// and what it looks like is what the photographer is
// confirming.
// As tall as the picture needs up to most of the page,
// and as wide as it is at that height: a four-frame
// perspective merge is nearly square, a twelve-frame
// cylinder is a strip, and either should fill what it
// can rather than sit in a letterbox.
if root.preview.width > 0: Rectangle {
height: min(root.height * 0.55, parent.width * root.preview.height / max(1, root.preview.width));
background: Theme.rule;
Image {
source: root.preview;
width: 100%;
height: 100%;
image-fit: contain;
}
}
Caption { text: root.summary; wrap: word-wrap; }
Caption {
text: "Each frame is outlined in amber where it lands. Ghosting in an overlap is the alignment's error.";
wrap: word-wrap;
}
Segmented {
label: "Projection";
options: root.projection-labels;
selected: root.projection-selected;
enabled: !root.running && !root.done;
picked(i) => { root.projection-picked(i); }
}
Segmented {
label: "Border";
options: root.border-labels;
selected: root.border-selected;
enabled: !root.running && !root.done && root.fill-available;
picked(i) => { root.border-picked(i); }
}
// Said plainly, because these are pixels the camera never
// saw: what the fill is, where it runs, and that the
// composite will say it was filled.
Caption {
text: root.fill-available
? (root.preview-filled
? "The border above is filled with pixels the model invented from the picture beside them (" + root.fill-note + "). The composite records that it was filled."
: "Filling the border invents pixels from the picture beside them (" + root.fill-note + "). Choose it to see what it would do before merging.")
: root.fill-note;
wrap: word-wrap;
}
// The knobs, while the fill is experimental: turned
// under the fill, the preview redraws with them.
if root.fill-available && root.border-selected == 1 : VerticalLayout {
spacing: 4px;
Caption { text: "Experimental — the fill's knobs. Each redraws the preview."; wrap: word-wrap; }
SliderRow {
label: "Working scale";
hint: "the composite's resolution over this: 2 is half, 4 a quarter";
value: root.fill-scale; default-value: 2; minimum: 1; maximum: 8;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("scale", v); }
reset => { root.fill-knob("scale", 2); }
}
SliderRow {
label: "Edge erosion";
hint: "working pixels of the coverage edge regenerated as fringe";
value: root.fill-erosion; default-value: 4; minimum: 0; maximum: 64;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("erosion", v); }
reset => { root.fill-knob("erosion", 4); }
}
SliderRow {
label: "Coarse pass";
hint: "reduction of the structure pass; 1 skips it";
value: root.fill-coarse; default-value: 1; minimum: 1; maximum: 8;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("coarse", v); }
reset => { root.fill-knob("coarse", 1); }
}
SliderRow {
label: "Band width";
hint: "the fine passes' bands from the edge outward, working pixels";
value: root.fill-band; default-value: 192; minimum: 16; maximum: 384;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("band", v); }
reset => { root.fill-knob("band", 192); }
}
SliderRow {
label: "Mirror depth";
hint: "0: the void as it is, what the model was trained on; otherwise how far into the picture a mirrored context reaches";
value: root.fill-mirror; default-value: 0; minimum: 0; maximum: 512;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("mirror", v); }
reset => { root.fill-knob("mirror", 0); }
}
SliderRow {
label: "Seam feather";
hint: "working pixels inside the real edge blended with the fill; 0 is a hard cut";
value: root.fill-feather; default-value: 48; minimum: 0; maximum: 256;
enabled: !root.running && !root.done;
changed(v) => { root.fill-knob("feather", v); }
reset => { root.fill-knob("feather", 48); }
}
}
}
// --- the frames ----------------------------------------
Panel {
PanelHeading { text: "Frames"; }
for row in root.frames: HorizontalLayout {
spacing: Theme.gap;
Label {
text: row.name;
width: 160px;
overflow: elide;
}
Caption {
text: row.detail;
warn: !row.aligned;
overflow: elide;
horizontal-stretch: 1;
}
}
if root.frames.length == 0: Caption {
text: "Reading the frames…";
}
}
if root.status != "": Caption {
text: root.status;
wrap: word-wrap;
}
}
}
}
}