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:
2026-08-22 08:39:17 +02:00
parent ec713585a5
commit 6163b63895
4 changed files with 223 additions and 1 deletions
+80
View File
@@ -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);