Show the regions, click one, and adjust it
The local panel sits above the adjust panel because it decides what those sliders act on; below it, a photographer would set an exposure and only then discover which scope it landed in. Selecting a layer re-scopes the existing controls to that layer's chain — there is no second set of sliders, and there must not be, or every operation added to `ops/` would need a local twin. The overlay is drawn over the canvas rather than blended into the render, because it is a diagnostic and not an edit: it must not reach the histogram, an export, or the texture handed to the compositor. Nearest- neighbour always — the map's values are *names*, so smoothing between region 4 and region 9 invents a colour belonging to neither and softens exactly the edge the overlay exists to show. Picking gets its own touch area above the pan handler. Panning wants press-drag-release and picking wants a click; interleaving them in one handler is how a drag ends up selecting a region the user was scrolling past. Shift is tracked as window state because a TouchArea's click carries no modifiers. Three states a layer can be in are worth distinguishing, and each has a different remedy: stale needs re-segmenting, "no adjustment yet" needs a slider moved, and the ordinary case needs nothing said. A bare selection renders nothing and looks identical to a broken mask, which is the first thing a new user will hit. Known rough edge, commented where it happens: segmentation blocks the UI thread for about half a second. Moving it to a worker needs the develop session — GPU resources behind a RefCell shared with every callback — to be reachable from another thread, which is a restructuring rather than a change to the call. The button says "Finding regions…" first so the stall is announced rather than looking like a hang.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Theme } from "theme.slint";
|
||||
import { AdjustPanel, GeometryPanel, ParamRow, TransferPanel } from "adjust.slint";
|
||||
import { MaskPanel, MaskRow, SubjectRow } from "masks.slint";
|
||||
import { LaunchScreen } from "launch.slint";
|
||||
import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll } from "library.slint";
|
||||
import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint";
|
||||
@@ -792,6 +793,51 @@ export component AppWindow inherits Window {
|
||||
in property <bool> expanded: true;
|
||||
in property <string> layout-class: "expanded";
|
||||
|
||||
// --- local adjustments (FR-DEV-3) ---------------------------------------
|
||||
|
||||
/// The false-coloured region map, drawn over the photograph.
|
||||
///
|
||||
/// An empty image when there is nothing to draw, which is also how the
|
||||
/// overlay is hidden — one source of truth rather than a flag that can
|
||||
/// disagree with the picture.
|
||||
in property <image> region-overlay;
|
||||
/// How strongly the overlay is composited. Not 1.0: the point of looking
|
||||
/// at it is to judge whether a boundary follows the *photograph*, and an
|
||||
/// opaque map hides the thing being judged.
|
||||
in property <float> overlay-strength: 0.55;
|
||||
in property <bool> overlay-on: false;
|
||||
/// Clicking the canvas selects a region instead of panning.
|
||||
in property <bool> region-picking: false;
|
||||
/// Whether shift is down, tracked by the develop key scope below.
|
||||
///
|
||||
/// A `TouchArea`'s `clicked` carries no modifiers, so the state has to be
|
||||
/// held between the key event and the click. Private to the window because
|
||||
/// nothing outside it has any business setting it.
|
||||
property <bool> shift-held: false;
|
||||
|
||||
in property <[MaskRow]> mask-rows;
|
||||
in property <[SubjectRow]> subject-rows;
|
||||
in property <bool> segmented: false;
|
||||
in property <bool> segmenting: false;
|
||||
in property <int> segmentation-level: 300;
|
||||
in property <int> segmentation-regions: 0;
|
||||
in property <bool> editing-mask: false;
|
||||
|
||||
callback segment-image();
|
||||
callback overlay-toggled(bool);
|
||||
callback region-picking-toggled(bool);
|
||||
callback segmentation-level-changed(int);
|
||||
/// A click on the photograph, in fractions of the shown image, plus
|
||||
/// whether it should extend the selection rather than replace it.
|
||||
callback region-picked(float, float, bool);
|
||||
callback mask-selected(string);
|
||||
callback mask-removed(string);
|
||||
callback mask-toggled(string, bool);
|
||||
callback mask-invert-toggled(string, bool);
|
||||
callback mask-opacity-changed(string, float);
|
||||
callback add-gradient-mask(bool);
|
||||
callback add-subject-mask(int);
|
||||
|
||||
/// Whether the develop column — image info, geometry, adjust — is shown.
|
||||
///
|
||||
/// Distinct from `expanded`, which is the *layout class* and only supplies
|
||||
@@ -1276,6 +1322,29 @@ in property <bool> panel-visible: true;
|
||||
visible: root.total > 0 && root.load-error == "";
|
||||
}
|
||||
|
||||
// The region map, composited over the photograph at the
|
||||
// same fitted rect.
|
||||
//
|
||||
// Drawn here rather than blended into the render because
|
||||
// it is a diagnostic and not an edit: it must not reach
|
||||
// the histogram, an export, or the texture the develop
|
||||
// pass hands the compositor.
|
||||
if root.overlay-on && root.total > 0: Image {
|
||||
x: parent.shown-x;
|
||||
y: parent.shown-y;
|
||||
width: parent.shown-w;
|
||||
height: parent.shown-h;
|
||||
source: root.region-overlay;
|
||||
image-fit: fill;
|
||||
opacity: root.overlay-strength;
|
||||
// Nearest-neighbour, always. The map is proxy-sized
|
||||
// and its values are *names*: smoothing between region
|
||||
// 4 and region 9 invents a colour belonging to
|
||||
// neither, and draws a soft edge exactly where the
|
||||
// point is to see a hard one.
|
||||
image-rendering: ImageRendering.pixelated;
|
||||
}
|
||||
|
||||
// Where the photograph actually sits inside this box.
|
||||
//
|
||||
// `image-fit: contain` letterboxes, and Slint does not report
|
||||
@@ -1344,6 +1413,31 @@ in property <bool> panel-visible: true;
|
||||
cancelled => { self.last-scale = 1.0; }
|
||||
}
|
||||
|
||||
// Region picking, above the pan/zoom area so a click
|
||||
// reaches it first. A separate area rather than a branch
|
||||
// inside the one below: panning wants press-drag-release
|
||||
// and picking wants a click, and interleaving the two in
|
||||
// one handler is how a drag ends up selecting a region the
|
||||
// user was only scrolling past.
|
||||
if root.region-picking && root.total > 0 && root.load-error == "": pick := TouchArea {
|
||||
x: parent.shown-x;
|
||||
y: parent.shown-y;
|
||||
width: parent.shown-w;
|
||||
height: parent.shown-h;
|
||||
mouse-cursor: MouseCursor.crosshair;
|
||||
enabled: !root.crop-mode;
|
||||
|
||||
clicked => {
|
||||
root.region-picked(
|
||||
self.mouse-x / max(self.width, 1px),
|
||||
self.mouse-y / max(self.height, 1px),
|
||||
// Shift extends or removes, the modifier every
|
||||
// selection tool uses for the same thing.
|
||||
root.shift-held,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if root.total > 0 && root.load-error == "": TouchArea {
|
||||
x: 0; y: 0;
|
||||
width: 100%;
|
||||
@@ -1595,7 +1689,18 @@ in property <bool> panel-visible: true;
|
||||
FocusScope {
|
||||
init => { self.focus(); }
|
||||
|
||||
// Shift is state, not an event: the click that needs
|
||||
// to know about it arrives through a `TouchArea`,
|
||||
// which reports no modifiers of its own. Tracked on
|
||||
// both edges so releasing it while the pointer is over
|
||||
// the canvas is noticed.
|
||||
key-released(event) => {
|
||||
root.shift-held = event.modifiers.shift;
|
||||
return reject;
|
||||
}
|
||||
|
||||
key-pressed(event) => {
|
||||
root.shift-held = event.modifiers.shift;
|
||||
// Ctrl+Z and Ctrl+Shift+Z (FR-DEV-5). Both cases
|
||||
// of the letter, because the logical key that
|
||||
// reaches us carries the shift: holding it for
|
||||
@@ -1786,6 +1891,44 @@ in property <bool> panel-visible: true;
|
||||
background: Theme.rule;
|
||||
}
|
||||
|
||||
// Above the adjust panel, because it decides what
|
||||
// those sliders act on. Below it, the photographer
|
||||
// would set an exposure and only then discover which
|
||||
// scope it landed in.
|
||||
MaskPanel {
|
||||
enabled: root.adjust-enabled;
|
||||
masks: root.mask-rows;
|
||||
subjects: root.subject-rows;
|
||||
segmented: root.segmented;
|
||||
segmenting: root.segmenting;
|
||||
level: root.segmentation-level;
|
||||
region-count: root.segmentation-regions;
|
||||
overlay: root.overlay-on;
|
||||
picking: root.region-picking;
|
||||
editing-mask: root.editing-mask;
|
||||
|
||||
segment => { root.segment-image(); }
|
||||
overlay-toggled(on) => { root.overlay-toggled(on); }
|
||||
picking-toggled(on) => { root.region-picking-toggled(on); }
|
||||
level-changed(v) => { root.segmentation-level-changed(v); }
|
||||
mask-selected(id) => { root.mask-selected(id); }
|
||||
mask-removed(id) => { root.mask-removed(id); }
|
||||
mask-toggled(id, on) => { root.mask-toggled(id, on); }
|
||||
mask-invert-toggled(id, on) => {
|
||||
root.mask-invert-toggled(id, on);
|
||||
}
|
||||
mask-opacity-changed(id, v) => {
|
||||
root.mask-opacity-changed(id, v);
|
||||
}
|
||||
add-gradient(radial) => { root.add-gradient-mask(radial); }
|
||||
add-subject(i) => { root.add-subject-mask(i); }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.rule;
|
||||
}
|
||||
|
||||
adjust := AdjustPanel {
|
||||
rows: root.adjust-rows;
|
||||
enabled: root.adjust-enabled;
|
||||
|
||||
Reference in New Issue
Block a user