Put the edge controls where the mask is
Feather, falloff, grow/shrink/close/open and their amount, on the selected layer. All four read the one distance field, so all four are live — nothing recomputes except a compound morphology, and the session keys on that separately so a feather drag rebuilds nothing. Shown only for sources that go through the distance field. A gradient carries its own falloff in its geometry, and offering a second one would be two controls fighting over the same edge. Picking an operation seeds a small amount if none is set. Selecting "Grow" and seeing nothing happen would read as a broken control rather than as a radius of zero.
This commit is contained in:
@@ -1105,6 +1105,86 @@ impl DevelopSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// Edge transition half-width, in fractions of the shorter edge.
|
||||
pub fn set_mask_feather(&mut self, id: &str, feather: f32) {
|
||||
if let Some(layer) = self.graph.masks_mut().get_mut(id) {
|
||||
layer.feather = feather.clamp(0.0, 1.0);
|
||||
self.history.record(&self.graph, Edit::Op(OpId("mask-feather")));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mask_falloff(&mut self, id: &str, index: usize) {
|
||||
use dr_pipeline::mask::Falloff;
|
||||
let Some(&falloff) = Falloff::ALL.get(index) else {
|
||||
return;
|
||||
};
|
||||
if let Some(layer) = self.graph.masks_mut().get_mut(id) {
|
||||
layer.falloff = falloff;
|
||||
self.history.record(&self.graph, Edit::Discrete);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mask_morphology(&mut self, id: &str, index: usize) {
|
||||
use dr_pipeline::mask::Morphology;
|
||||
let Some(&morphology) = Morphology::ALL.get(index) else {
|
||||
return;
|
||||
};
|
||||
if let Some(layer) = self.graph.masks_mut().get_mut(id) {
|
||||
layer.morphology = morphology;
|
||||
// Picking an operation with no amount set would appear to do
|
||||
// nothing, and the user would reasonably conclude it is broken.
|
||||
if morphology != Morphology::None && layer.morph_radius <= 0.0 {
|
||||
layer.morph_radius = 0.006;
|
||||
}
|
||||
self.history.record(&self.graph, Edit::Discrete);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_mask_morph_radius(&mut self, id: &str, radius: f32) {
|
||||
if let Some(layer) = self.graph.masks_mut().get_mut(id) {
|
||||
layer.morph_radius = radius.clamp(0.0, 1.0);
|
||||
self.history.record(&self.graph, Edit::Op(OpId("mask-morph")));
|
||||
}
|
||||
}
|
||||
|
||||
/// Which falloff a layer uses, as an index into `Falloff::ALL`.
|
||||
pub fn mask_falloff(&self, id: &str) -> usize {
|
||||
use dr_pipeline::mask::Falloff;
|
||||
self.graph.masks().get(id).map_or(0, |l| {
|
||||
Falloff::ALL.iter().position(|&f| f == l.falloff).unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mask_morphology(&self, id: &str) -> usize {
|
||||
use dr_pipeline::mask::Morphology;
|
||||
self.graph.masks().get(id).map_or(0, |l| {
|
||||
Morphology::ALL
|
||||
.iter()
|
||||
.position(|&m| m == l.morphology)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mask_feather(&self, id: &str) -> f32 {
|
||||
self.graph.masks().get(id).map_or(0.0, |l| l.feather)
|
||||
}
|
||||
|
||||
pub fn mask_morph_radius(&self, id: &str) -> f32 {
|
||||
self.graph.masks().get(id).map_or(0.0, |l| l.morph_radius)
|
||||
}
|
||||
|
||||
/// Whether the edge controls apply to this layer.
|
||||
///
|
||||
/// Only sources that go through the distance field. A gradient carries its
|
||||
/// own falloff in its geometry, so offering a second one would be two
|
||||
/// controls fighting over the same edge.
|
||||
pub fn mask_is_shapeable(&self, id: &str) -> bool {
|
||||
use dr_pipeline::mask::MaskSource;
|
||||
self.graph.masks().get(id).is_some_and(|l| {
|
||||
matches!(l.source, MaskSource::Subject { .. } | MaskSource::Regions { .. })
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_mask_opacity(&mut self, id: &str, opacity: f32) {
|
||||
if let Some(layer) = self.graph.masks_mut().get_mut(id) {
|
||||
layer.opacity = opacity.clamp(0.0, 1.0);
|
||||
|
||||
@@ -47,6 +47,11 @@ pub(crate) fn sync(window: &AppWindow, session: &Rc<RefCell<Option<DevelopSessio
|
||||
kind: s.mask_kind(&id).into(),
|
||||
inverted: s.mask_inverted(&id),
|
||||
opacity: s.mask_opacity(&id),
|
||||
feather: s.mask_feather(&id),
|
||||
falloff: s.mask_falloff(&id) as i32,
|
||||
morphology: s.mask_morphology(&id) as i32,
|
||||
morph_radius: s.mask_morph_radius(&id),
|
||||
shapeable: s.mask_is_shapeable(&id),
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
enabled,
|
||||
@@ -280,6 +285,64 @@ pub(crate) fn wire(
|
||||
});
|
||||
}
|
||||
|
||||
// --- the edge treatment -------------------------------------------------
|
||||
//
|
||||
// All four read one distance field, so all four are live: nothing here
|
||||
// rebuilds anything except a compound morphology, which `develop` keys on
|
||||
// separately.
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window.on_mask_feather_changed(move |id, value| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.set_mask_feather(&id, value);
|
||||
}
|
||||
sync(&w, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window.on_mask_falloff_picked(move |id, index| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.set_mask_falloff(&id, index.max(0) as usize);
|
||||
}
|
||||
sync(&w, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window.on_mask_morphology_picked(move |id, index| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.set_mask_morphology(&id, index.max(0) as usize);
|
||||
}
|
||||
sync(&w, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
let session = session.clone();
|
||||
let redraw = redraw.clone();
|
||||
window.on_mask_morph_radius_changed(move |id, value| {
|
||||
let Some(w) = weak.upgrade() else { return };
|
||||
if let Some(s) = session.borrow_mut().as_mut() {
|
||||
s.set_mask_morph_radius(&id, value);
|
||||
}
|
||||
sync(&w, &session);
|
||||
redraw(&w);
|
||||
});
|
||||
}
|
||||
|
||||
// --- adding layers ------------------------------------------------------
|
||||
{
|
||||
let weak = window.as_weak();
|
||||
|
||||
@@ -836,6 +836,10 @@ export component AppWindow inherits Window {
|
||||
callback mask-toggled(string, bool);
|
||||
callback mask-invert-toggled(string, bool);
|
||||
callback mask-opacity-changed(string, float);
|
||||
callback mask-feather-changed(string, float);
|
||||
callback mask-falloff-picked(string, int);
|
||||
callback mask-morphology-picked(string, int);
|
||||
callback mask-morph-radius-changed(string, float);
|
||||
callback add-gradient-mask(bool);
|
||||
callback add-subject-mask(int);
|
||||
|
||||
@@ -1922,6 +1926,18 @@ in property <bool> panel-visible: true;
|
||||
mask-opacity-changed(id, v) => {
|
||||
root.mask-opacity-changed(id, v);
|
||||
}
|
||||
mask-feather-changed(id, v) => {
|
||||
root.mask-feather-changed(id, v);
|
||||
}
|
||||
mask-falloff-picked(id, i) => {
|
||||
root.mask-falloff-picked(id, i);
|
||||
}
|
||||
mask-morphology-picked(id, i) => {
|
||||
root.mask-morphology-picked(id, i);
|
||||
}
|
||||
mask-morph-radius-changed(id, v) => {
|
||||
root.mask-morph-radius-changed(id, v);
|
||||
}
|
||||
add-gradient(radial) => { root.add-gradient-mask(radial); }
|
||||
add-subject(i) => { root.add-subject-mask(i); }
|
||||
}
|
||||
|
||||
+64
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
import { Theme } from "theme.slint";
|
||||
import { Button, Caption, IconButton, Label, PanelHeading, Value } from "widgets.slint";
|
||||
import { SliderRow } from "controls.slint";
|
||||
import { Segmented, SliderRow } from "controls.slint";
|
||||
|
||||
/// One layer in the stack.
|
||||
export struct MaskRow {
|
||||
@@ -22,6 +22,17 @@ export struct MaskRow {
|
||||
selected: bool,
|
||||
inverted: bool,
|
||||
opacity: float,
|
||||
/// Edge transition half-width, as a fraction of the frame's shorter edge.
|
||||
feather: float,
|
||||
/// Index into `Falloff::ALL`.
|
||||
falloff: int,
|
||||
/// Index into `Morphology::ALL`.
|
||||
morphology: int,
|
||||
morph-radius: float,
|
||||
/// Whether the edge controls apply. A gradient has its own geometry and
|
||||
/// does not go through the distance field, so showing them would offer
|
||||
/// controls that do nothing.
|
||||
shapeable: bool,
|
||||
/// Its region ids came from a different segmentation than the one loaded,
|
||||
/// so applying it would produce a confidently wrong shape.
|
||||
stale: bool,
|
||||
@@ -49,6 +60,10 @@ component MaskEntry inherits Rectangle {
|
||||
callback toggled(bool);
|
||||
callback invert-toggled(bool);
|
||||
callback opacity-changed(float);
|
||||
callback feather-changed(float);
|
||||
callback falloff-picked(int);
|
||||
callback morphology-picked(int);
|
||||
callback morph-radius-changed(float);
|
||||
|
||||
height: layout.preferred-height;
|
||||
background: root.data.selected ? Theme.surface-raised : transparent;
|
||||
@@ -155,6 +170,46 @@ component MaskEntry inherits Rectangle {
|
||||
emphasised: invert.has-hover || root.data.inverted;
|
||||
}
|
||||
}
|
||||
|
||||
// The edge. All of it reads one signed distance field, so these
|
||||
// are free to move — none of them recomputes anything except a
|
||||
// compound morphology.
|
||||
if root.data.shapeable: SliderRow {
|
||||
label: "Feather";
|
||||
hint: "How far the edit fades across the edge.";
|
||||
value: root.data.feather * 1000;
|
||||
default-value: 4;
|
||||
minimum: 0;
|
||||
maximum: 60;
|
||||
changed(v) => { root.feather-changed(v / 1000); }
|
||||
reset => { root.feather-changed(0.004); }
|
||||
}
|
||||
|
||||
if root.data.shapeable: Segmented {
|
||||
label: "Falloff";
|
||||
options: ["Hard", "Linear", "Smooth", "Gaussian", "Expo"];
|
||||
selected: root.data.falloff;
|
||||
picked(i) => { root.falloff-picked(i); }
|
||||
}
|
||||
|
||||
if root.data.shapeable: Segmented {
|
||||
label: "Edge";
|
||||
hint: "Grow or shrink the outline the model drew. Close fills "
|
||||
+ "pinholes; open removes specks.";
|
||||
options: ["None", "Grow", "Shrink", "Close", "Open"];
|
||||
selected: root.data.morphology;
|
||||
picked(i) => { root.morphology-picked(i); }
|
||||
}
|
||||
|
||||
if root.data.shapeable && root.data.morphology != 0: SliderRow {
|
||||
label: "Amount";
|
||||
value: root.data.morph-radius * 1000;
|
||||
default-value: 0;
|
||||
minimum: 0;
|
||||
maximum: 40;
|
||||
changed(v) => { root.morph-radius-changed(v / 1000); }
|
||||
reset => { root.morph-radius-changed(0); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,6 +254,10 @@ export component MaskPanel inherits Rectangle {
|
||||
callback mask-toggled(string, bool);
|
||||
callback mask-invert-toggled(string, bool);
|
||||
callback mask-opacity-changed(string, float);
|
||||
callback mask-feather-changed(string, float);
|
||||
callback mask-falloff-picked(string, int);
|
||||
callback mask-morphology-picked(string, int);
|
||||
callback mask-morph-radius-changed(string, float);
|
||||
|
||||
callback add-gradient(bool);
|
||||
callback add-subject(int);
|
||||
@@ -373,6 +432,10 @@ export component MaskPanel inherits Rectangle {
|
||||
toggled(on) => { root.mask-toggled(mask.id, on); }
|
||||
invert-toggled(on) => { root.mask-invert-toggled(mask.id, on); }
|
||||
opacity-changed(v) => { root.mask-opacity-changed(mask.id, v); }
|
||||
feather-changed(v) => { root.mask-feather-changed(mask.id, v); }
|
||||
falloff-picked(i) => { root.mask-falloff-picked(mask.id, i); }
|
||||
morphology-picked(i) => { root.mask-morphology-picked(mask.id, i); }
|
||||
morph-radius-changed(v) => { root.mask-morph-radius-changed(mask.id, v); }
|
||||
}
|
||||
|
||||
// The one thing this panel has to say about the adjust panel below it,
|
||||
|
||||
Reference in New Issue
Block a user