WIP: capture sharpening

Checkpoint committed by the coordinator, not by the authoring agent: the
session hit its API limit mid-task and left this work uncommitted. Committed
so it survives, NOT because it is finished - expect failing tests and
half-applied changes. The agent resumes from here.
This commit is contained in:
2026-08-22 19:01:18 +02:00
parent c963dafd09
commit 8ea427de3c
7 changed files with 1410 additions and 2 deletions
+44
View File
@@ -934,9 +934,19 @@ impl MaskLayer {
/// the output's dimensions, so it is a property of the photograph and not
/// of a region within it. There is no such thing as cropping part of an
/// image.
///
/// The neighbourhood operations are absent for the same reason, and this
/// filter is the visible half of the one [`Self::active_ops`] already
/// applies. A layer's chain is fused into the point-operation pass; the
/// detail stage runs once, afterwards, over the whole frame, so there is
/// no seam through which a mask could reach it (see [`crate::detail`]).
/// Offering the controls anyway would put a sharpening slider on a mask
/// that moves and does nothing — which is worse than the control being
/// absent, because absence is legible and a dead slider is not.
pub fn capabilities(&self) -> Vec<crate::graph::OpCapability> {
self.ops
.iter()
.filter(|op| op.detail().is_none())
.map(|op| {
let desc = op.descriptor();
crate::graph::OpCapability {
@@ -1625,4 +1635,38 @@ mod tests {
let order: Vec<&str> = stack.layers().iter().map(|l| l.id.as_str()).collect();
assert_eq!(order, ["m3", "m1", "m2"]);
}
#[test]
fn a_layer_offers_no_control_it_cannot_honour() {
// A layer's chain is fused into the point-operation pass, and the
// detail stage runs once afterwards over the whole frame — so a
// neighbourhood operation inside a mask has nowhere to run.
// `active_ops` has always dropped them; this is the other half, which
// stops the panel drawing a sharpening slider on a mask that would
// move and change nothing.
let layer = lit_layer("m1", 1.0);
let global: Vec<&str> = crate::EditGraph::default_chain()
.capabilities()
.iter()
.map(|c| c.id.0)
.collect();
let scoped: Vec<&str> = layer.capabilities().iter().map(|c| c.id.0).collect();
let detail: Vec<&str> = crate::ops::chain()
.iter()
.filter(|o| o.detail().is_some())
.map(|o| o.descriptor().id.0)
.collect();
assert!(
!detail.is_empty(),
"the chain has neighbourhood operations, or this proves nothing"
);
for id in detail {
assert!(global.contains(&id), "{id} is missing from the chain");
assert!(
!scoped.contains(&id),
"{id} cannot run inside a mask and must not be offered there"
);
}
}
}