diff --git a/core/dr-gpu/src/mask.rs b/core/dr-gpu/src/mask.rs
index 485ddf3..c96c346 100644
--- a/core/dr-gpu/src/mask.rs
+++ b/core/dr-gpu/src/mask.rs
@@ -395,6 +395,7 @@ pub struct MaskPass {
combine_layout: wgpu::BindGroupLayout,
combine_union: wgpu::RenderPipeline,
combine_subtract: wgpu::RenderPipeline,
+ combine_intersect: wgpu::RenderPipeline,
/// Where a part is drawn before it is joined.
///
/// One texture for the whole stack rather than one per layer, because
@@ -651,6 +652,16 @@ impl MaskPass {
"mask-combine-subtract",
blend_state(wgpu::BlendFactor::Zero, wgpu::BlendFactor::OneMinusSrc),
);
+ // TRACES: FR-DEV-19a
+ // `dst * src`: what the mask had, kept only in proportion to how much
+ // of it this part also covers. The same three vertices and the same
+ // scratch, so a third set operation is a third blend state and
+ // nothing more — which is what `Join::apply` states on the CPU and
+ // `the_joins_match_their_definition` holds this to.
+ let combine_intersect = combine(
+ "mask-combine-intersect",
+ blend_state(wgpu::BlendFactor::Zero, wgpu::BlendFactor::Src),
+ );
// The same, with the deposit thrown away: coverage is only ever taken
// off what earlier strokes on this layer put down. There is no negative
@@ -681,6 +692,7 @@ impl MaskPass {
combine_layout,
combine_union,
combine_subtract,
+ combine_intersect,
scratch: None,
array: None,
allocations: 0,
@@ -1376,6 +1388,7 @@ impl MaskPass {
pass.set_pipeline(match join {
Join::Union => &self.combine_union,
Join::Subtract => &self.combine_subtract,
+ Join::Intersect => &self.combine_intersect,
});
pass.set_bind_group(0, &bind_group, &[]);
pass.draw(0..3, 0..1);
diff --git a/core/dr-gpu/tests/local_adjustments.rs b/core/dr-gpu/tests/local_adjustments.rs
index 084b881..5b8653b 100644
--- a/core/dr-gpu/tests/local_adjustments.rs
+++ b/core/dr-gpu/tests/local_adjustments.rs
@@ -790,6 +790,119 @@ fn the_order_parts_are_joined_in_is_the_mask() {
);
}
+/// TRACES: FR-DEV-19a
+/// The truth table `docs/dev/mask-editing.md` §13 asks for: one base, one
+/// part that half-covers it, joined each of the three ways. The base is the
+/// left half of the frame and the part a dab in the middle, so the four
+/// quarters of the table are four pixels.
+#[test]
+fn a_part_unioned_subtracted_and_intersected_gives_the_three_fields() {
+ let Some(ctx) = ctx() else {
+ eprintln!("no adapter; skipping");
+ return;
+ };
+
+ let field = split_field(&ctx);
+ let joined = |join: Join| {
+ let mut layer = brighten(MaskSource::Regions {
+ signature: 1,
+ level: 2,
+ ids: vec![0],
+ });
+ assert!(layer.push_part(MaskPart::painted("p2", join)));
+ paint(&mut layer, 1, false, &[(0.5, 0.5)]);
+ let mut stack = MaskStack::new();
+ stack.push(layer);
+ render(&ctx, &stack, Some(&field))
+ };
+
+ // (base, part): left outside the dab, left inside, right inside, right
+ // outside.
+ let cells = [(4, 16), (14, 16), (18, 16), (27, 16)];
+ let lit = |pixels: &[u8]| cells.map(|(x, y)| luma_at(pixels, x, y) > 200);
+
+ assert_eq!(
+ lit(&joined(Join::Union)),
+ [true, true, true, false],
+ "union: either"
+ );
+ assert_eq!(
+ lit(&joined(Join::Subtract)),
+ [true, false, false, false],
+ "subtract: the base without the dab"
+ );
+ assert_eq!(
+ lit(&joined(Join::Intersect)),
+ [false, true, false, false],
+ "intersect: only where both are"
+ );
+}
+
+/// TRACES: FR-DEV-19a
+/// Intersection on soft coverage is the product `Join::apply` defines, on
+/// either side of the join: a gradient intersected with a region it fills is
+/// the gradient there and nothing elsewhere, and a region intersected with a
+/// gradient is the gradient wherever the region is.
+#[test]
+fn the_joins_match_their_definition() {
+ let Some(ctx) = ctx() else {
+ eprintln!("no adapter; skipping");
+ return;
+ };
+
+ let field = split_field(&ctx);
+ let ramp = || MaskSource::Linear {
+ centre: (0.5, 0.5),
+ angle: 0.0,
+ width: 1.0,
+ };
+ let right_half = || MaskSource::Regions {
+ signature: 1,
+ level: 2,
+ ids: vec![1],
+ };
+ let draw = |layer: MaskLayer| {
+ let mut stack = MaskStack::new();
+ stack.push(layer);
+ render(&ctx, &stack, Some(&field))
+ };
+
+ let alone = draw(brighten(ramp()));
+
+ let mut ramp_then_region = brighten(ramp());
+ assert!(ramp_then_region.push_part(MaskPart::new("p2", Join::Intersect, right_half())));
+ let ramp_then_region = draw(ramp_then_region);
+
+ let mut region_then_ramp = brighten(whole_frame());
+ assert!(region_then_ramp.push_part(MaskPart::new("p2", Join::Intersect, ramp())));
+ let region_then_ramp = draw(region_then_ramp);
+
+ for x in 0..SIZE {
+ let y = SIZE / 2;
+ let want = luma_at(&alone, x, y);
+ // dst · 1 = dst on the right; dst · 0 = 0 on the left. Pixels
+ // within two of the seam are left out: the region's own edge
+ // is soft there, so neither side of the table is 0 or 1.
+ let got = luma_at(&ramp_then_region, x, y);
+ if x.abs_diff(SIZE / 2) <= 2 {
+ // The seam.
+ } else if x > SIZE / 2 {
+ assert!(
+ got.abs_diff(want) <= 1,
+ "x={x}: the ramp survives where the region is ({got} vs {want})"
+ );
+ } else {
+ assert_eq!(got, 128, "x={x}: and nothing survives where it is not");
+ }
+ // 1 · src = src everywhere.
+ let got = luma_at(®ion_then_ramp, x, y);
+ assert!(
+ got.abs_diff(want) <= 1,
+ "x={x}: a full base intersected with the ramp is the ramp ({got} vs {want})"
+ );
+ }
+}
+
// --- seeing the mask (FR-DEV-19c) ------------------------------------------
/// A radial that covers the middle of the frame and nothing near the corners.
diff --git a/core/dr-pipeline/src/mask.rs b/core/dr-pipeline/src/mask.rs
index ff59aaf..59fb088 100644
--- a/core/dr-pipeline/src/mask.rs
+++ b/core/dr-pipeline/src/mask.rs
@@ -926,6 +926,21 @@ pub enum Join {
/// erase stroke is a hole in the part it was painted into and reads as
/// nothing at all.
Subtract,
+ /// TRACES: FR-DEV-19a
+ /// Only where both agree. "Keep the part of this mask that is also that."
+ ///
+ /// The join that makes cheap criteria precise: a sky is a category *and*
+ /// a luminance band, skin is a subject *and* a hue. Neither alone is the
+ /// selection, and no feather on either makes it one.
+ ///
+ /// The product of the two coverages rather than their minimum, because
+ /// that is what one fixed-function blend gives on the device
+ /// (`dst · src`, `docs/dev/mask-editing.md` §5.2) and it agrees with the
+ /// minimum wherever either side is fully in or fully out. Between two soft
+ /// edges it is the softer of the two readings, which is the right way to
+ /// be wrong: an overlap of two partial selections is less certainly
+ /// selected than either.
+ Intersect,
}
impl Join {
@@ -933,6 +948,7 @@ impl Join {
match self {
Self::Union => "union",
Self::Subtract => "subtract",
+ Self::Intersect => "intersect",
}
}
@@ -940,12 +956,30 @@ impl Join {
Some(match name {
"union" => Self::Union,
"subtract" => Self::Subtract,
+ "intersect" => Self::Intersect,
_ => return None,
})
}
- /// Every variant, for a UI building a choice control.
- pub const ALL: [Join; 2] = [Join::Union, Join::Subtract];
+ /// TRACES: FR-DEV-19a
+ /// The coverage this join leaves at one point, given what the mask had
+ /// there (`dst`) and what the part covers (`src`), both in `0..=1`.
+ ///
+ /// The definition the device's blend states implement, spelled out once
+ /// on the CPU so a test can hold the GPU to it and a reader can see the
+ /// three set operations side by side without reading `wgpu` enums.
+ pub fn apply(self, dst: f32, src: f32) -> f32 {
+ match self {
+ Self::Union => dst.max(src),
+ Self::Subtract => dst * (1.0 - src),
+ Self::Intersect => dst * src,
+ }
+ }
+
+ /// Every variant, for a UI building a choice control. The order is the
+ /// panel's: a chip cycles through it and a stored index names a place in
+ /// it, so a new join goes on the end.
+ pub const ALL: [Join; 3] = [Join::Union, Join::Subtract, Join::Intersect];
}
/// One selection inside a layer's mask.
@@ -1580,9 +1614,20 @@ impl MaskLayer {
// A hidden part is not in the build, whichever way it joins — and a
// hidden base hands its role to the first part that is shown, which
// is why "adds" is asked of the shown parts rather than of index 0.
+ //
+ // Folded rather than asked with `any`, because an intersection can
+ // take away everything the parts before it added: a subject
+ // intersected with an unpainted brush covers nothing. An inverted
+ // part is taken to cover, whatever its source — an inverted empty
+ // brush is the whole frame, and saying "covers nothing" of a layer
+ // that does would hide an adjustment the photographer made.
self.shown_parts()
.enumerate()
- .any(|(i, p)| (i == 0 || p.join == Join::Union) && p.covers())
+ .fold(false, |acc, (i, p)| match (i, p.join) {
+ (0, _) | (_, Join::Union) => acc || p.covers(),
+ (_, Join::Subtract) => acc,
+ (_, Join::Intersect) => acc && (p.covers() || p.invert),
+ })
}
/// TRACES: FR-DEV-19a
@@ -3037,6 +3082,69 @@ mod tests {
);
}
+ /// TRACES: FR-DEV-19a
+ /// The three joins, pointwise, on every pair of coverages a part and a
+ /// mask can meet at: fully in, fully out, and each soft edge. Intersection
+ /// is the product, which agrees with the minimum wherever either side is
+ /// decided and is the softer reading where both are not.
+ #[test]
+ fn the_joins_are_max_cut_and_product() {
+ let levels = [0.0f32, 0.25, 0.5, 0.75, 1.0];
+ for &dst in &levels {
+ for &src in &levels {
+ assert_eq!(Join::Union.apply(dst, src), dst.max(src));
+ assert_eq!(Join::Subtract.apply(dst, src), dst * (1.0 - src));
+ let meet = Join::Intersect.apply(dst, src);
+ assert_eq!(meet, dst * src);
+ assert!(meet <= dst.min(src), "never more than either side");
+ if dst == 0.0 || dst == 1.0 || src == 0.0 || src == 1.0 {
+ assert_eq!(meet, dst.min(src), "the minimum where either is decided");
+ }
+ }
+ }
+ }
+
+ /// A stored index names a place in [`Join::ALL`], and a sidecar names a
+ /// join by word: both have to survive a third join arriving, which means
+ /// the first two keep their places and every name reads back as itself.
+ #[test]
+ fn every_join_reads_back_by_name_and_keeps_its_place() {
+ assert_eq!(Join::ALL[0], Join::Union);
+ assert_eq!(Join::ALL[1], Join::Subtract);
+ for join in Join::ALL {
+ assert_eq!(Join::from_name(join.name()), Some(join));
+ }
+ assert_eq!(Join::from_name("intersect"), Some(Join::Intersect));
+ }
+
+ /// TRACES: FR-DEV-19a
+ /// An intersection can empty a mask the parts before it filled: a range
+ /// meeting an unpainted brush selects nothing, and rasterising it would
+ /// spend a slice to draw an empty field. Painting the brush, or inverting
+ /// it, gives the intersection something to keep.
+ #[test]
+ fn an_intersection_with_nothing_covers_nothing() {
+ let mut layer = MaskLayer::new("m1", MaskSource::highlights());
+ layer.set_param("exposure", ParamId("exposure"), 1.0);
+ layer.push_part(MaskPart::painted("p2", Join::Intersect));
+ assert!(
+ !layer.is_active(),
+ "a range intersected with an unpainted brush selects nothing"
+ );
+
+ layer.part_mut(1).expect("p2").invert = true;
+ assert!(layer.is_active(), "inverted, the empty brush is everywhere");
+ layer.part_mut(1).expect("p2").invert = false;
+
+ layer.begin_stroke(1, false, 0.1, 0.5, 1.0);
+ layer.extend_stroke(1, 0.5, 0.5);
+ layer.end_stroke(1);
+ assert!(layer.is_active(), "and painted, it keeps what it covers");
+
+ layer.part_mut(1).expect("p2").hidden = true;
+ assert!(layer.is_active(), "hidden, it is out of the build");
+ }
+
/// One stale part is a stale layer: the mask is the fold over all of them,
/// so a part that would draw a confidently wrong shape makes the result
/// wrong whichever way it joins.
diff --git a/core/dr-pipeline/tests/mask_sidecar.rs b/core/dr-pipeline/tests/mask_sidecar.rs
index fb0b18d..c0e8402 100644
--- a/core/dr-pipeline/tests/mask_sidecar.rs
+++ b/core/dr-pipeline/tests/mask_sidecar.rs
@@ -1344,6 +1344,28 @@ fn a_correction_painted_onto_a_subject_survives_a_round_trip() {
);
}
+/// TRACES: FR-DEV-19a
+/// An intersecting part is written under its own word and reads back as an
+/// intersection. A build from before intersection read that word as a union
+/// (see `an_unknown_join_adds_the_part`), which keeps the part visible and
+/// fixable rather than silently cutting the mask down.
+#[test]
+fn an_intersecting_part_survives_a_round_trip() {
+ let mut graph = EditGraph::default_chain();
+ graph.masks_mut().push(corrected("m1", Join::Intersect));
+
+ let mut sidecar = Sidecar::new();
+ sidecar.put(Version::from_graph("default", "Default", &graph));
+ let text = sidecar.to_text();
+ assert!(text.contains("join = intersect"), "{text}");
+
+ let restored = round_trip(&graph);
+ let layer = &restored.masks().layers()[0];
+ assert_eq!(layer.parts().len(), 2);
+ assert_eq!(layer.parts()[1].join, Join::Intersect);
+ assert_eq!(layer.parts()[0].join, Join::Union, "the base is untouched");
+}
+
/// TRACES: FR-DEV-19a
/// A part left out of the build comes back left out, and the file says so
/// under a word that cannot be confused with the layer's own switch.
diff --git a/docs/dev/traceability.md b/docs/dev/traceability.md
index dca134d..c4858f6 100644
--- a/docs/dev/traceability.md
+++ b/docs/dev/traceability.md
@@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
| Metric | Value |
|---|---|
| Source files scanned | 435 |
-| TRACES tags found | 1622 |
+| TRACES tags found | 1630 |
| Requirements defined | 188 |
| Requirements deferred (post-v1) | 24 |
| Requirements covered | 155 |
@@ -62,19 +62,19 @@ _None._
| FR-CULL-8a | [`core/dr-catalog/src/faces.rs:124`](../../core/dr-catalog/src/faces.rs#L124), [`core/dr-catalog/src/schema.rs:917`](../../core/dr-catalog/src/schema.rs#L917), [`core/dr-face/src/classify.rs:1`](../../core/dr-face/src/classify.rs#L1), [`core/dr-face/src/eyes.rs:1`](../../core/dr-face/src/eyes.rs#L1), [`core/dr-face/src/landmarks.rs:1`](../../core/dr-face/src/landmarks.rs#L1), [`core/dr-types/src/place.rs:133`](../../core/dr-types/src/place.rs#L133), [`ui/dr-ui/src/faces.rs:166`](../../ui/dr-ui/src/faces.rs#L166), [`ui/dr-ui/src/faces.rs:1821`](../../ui/dr-ui/src/faces.rs#L1821), [`ui/dr-ui/src/faces.rs:308`](../../ui/dr-ui/src/faces.rs#L308), [`ui/dr-ui/src/identity.rs:104`](../../ui/dr-ui/src/identity.rs#L104), [`ui/dr-ui/src/library/filters.rs:136`](../../ui/dr-ui/src/library/filters.rs#L136), [`ui/dr-ui/src/library/filters.rs:720`](../../ui/dr-ui/src/library/filters.rs#L720), [`ui/dr-ui/src/library/filters.rs:763`](../../ui/dr-ui/src/library/filters.rs#L763), [`ui/dr-ui/src/library/filters.rs:801`](../../ui/dr-ui/src/library/filters.rs#L801), [`ui/dr-ui/src/repairs.rs:571`](../../ui/dr-ui/src/repairs.rs#L571), [`ui/dr-ui/ui/identity.slint:273`](../../ui/dr-ui/ui/identity.slint#L273), [`ui/dr-ui/ui/identity.slint:63`](../../ui/dr-ui/ui/identity.slint#L63), [`ui/dr-ui/ui/library.slint:1805`](../../ui/dr-ui/ui/library.slint#L1805), [`ui/dr-ui/ui/library.slint:693`](../../ui/dr-ui/ui/library.slint#L693) |
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:1011`](../../core/dr-catalog/src/schema.rs#L1011), [`core/dr-catalog/src/schema.rs:781`](../../core/dr-catalog/src/schema.rs#L781), [`core/dr-face/src/assign.rs:1`](../../core/dr-face/src/assign.rs#L1), [`core/dr-face/src/neighbours.rs:1`](../../core/dr-face/src/neighbours.rs#L1), [`core/dr-types/src/settings.rs:129`](../../core/dr-types/src/settings.rs#L129), [`ui/dr-ui/src/faces.rs:1`](../../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/faces.rs:584`](../../ui/dr-ui/src/faces.rs#L584), [`ui/dr-ui/src/identity_ui.rs:1`](../../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/repairs.rs:517`](../../ui/dr-ui/src/repairs.rs#L517), [`ui/dr-ui/ui/identity.slint:347`](../../ui/dr-ui/ui/identity.slint#L347) |
| FR-DEV-1 | [`core/dr-pipeline/src/graph.rs:1`](../../core/dr-pipeline/src/graph.rs#L1), [`core/dr-pipeline/src/sidecar.rs:1`](../../core/dr-pipeline/src/sidecar.rs#L1) |
-| FR-DEV-10 | [`core/dr-gpu/src/mask.rs:1027`](../../core/dr-gpu/src/mask.rs#L1027), [`core/dr-gpu/src/mask.rs:1251`](../../core/dr-gpu/src/mask.rs#L1251), [`core/dr-gpu/src/mask.rs:1509`](../../core/dr-gpu/src/mask.rs#L1509), [`core/dr-gpu/src/mask.rs:421`](../../core/dr-gpu/src/mask.rs#L421), [`core/dr-gpu/src/mask.rs:463`](../../core/dr-gpu/src/mask.rs#L463), [`core/dr-gpu/src/mask.rs:58`](../../core/dr-gpu/src/mask.rs#L58), [`core/dr-gpu/src/mask.rs:60`](../../core/dr-gpu/src/mask.rs#L60), [`core/dr-gpu/src/mask.rs:834`](../../core/dr-gpu/src/mask.rs#L834), [`core/dr-gpu/src/mask.rs:85`](../../core/dr-gpu/src/mask.rs#L85), [`core/dr-gpu/src/mask.rs:913`](../../core/dr-gpu/src/mask.rs#L913), [`core/dr-gpu/src/shaders/mask.wgsl:117`](../../core/dr-gpu/src/shaders/mask.wgsl#L117), [`core/dr-gpu/src/shaders/mask.wgsl:59`](../../core/dr-gpu/src/shaders/mask.wgsl#L59), [`core/dr-gpu/tests/range_masks.rs:119`](../../core/dr-gpu/tests/range_masks.rs#L119), [`core/dr-gpu/tests/range_masks.rs:147`](../../core/dr-gpu/tests/range_masks.rs#L147), [`core/dr-gpu/tests/range_masks.rs:175`](../../core/dr-gpu/tests/range_masks.rs#L175), [`core/dr-gpu/tests/range_masks.rs:1`](../../core/dr-gpu/tests/range_masks.rs#L1), [`core/dr-gpu/tests/range_masks.rs:94`](../../core/dr-gpu/tests/range_masks.rs#L94), [`core/dr-pipeline/src/mask.rs:125`](../../core/dr-pipeline/src/mask.rs#L125), [`core/dr-pipeline/src/mask.rs:2845`](../../core/dr-pipeline/src/mask.rs#L2845), [`core/dr-pipeline/src/mask.rs:2858`](../../core/dr-pipeline/src/mask.rs#L2858), [`core/dr-pipeline/src/mask.rs:2877`](../../core/dr-pipeline/src/mask.rs#L2877), [`core/dr-pipeline/src/mask.rs:2891`](../../core/dr-pipeline/src/mask.rs#L2891), [`core/dr-pipeline/src/mask.rs:2901`](../../core/dr-pipeline/src/mask.rs#L2901), [`core/dr-pipeline/src/mask.rs:710`](../../core/dr-pipeline/src/mask.rs#L710), [`core/dr-pipeline/src/mask.rs:749`](../../core/dr-pipeline/src/mask.rs#L749), [`core/dr-pipeline/src/mask.rs:809`](../../core/dr-pipeline/src/mask.rs#L809), [`core/dr-pipeline/src/mask.rs:828`](../../core/dr-pipeline/src/mask.rs#L828), [`core/dr-pipeline/src/mask.rs:852`](../../core/dr-pipeline/src/mask.rs#L852), [`core/dr-pipeline/src/mask.rs:864`](../../core/dr-pipeline/src/mask.rs#L864), [`core/dr-pipeline/src/mask.rs:876`](../../core/dr-pipeline/src/mask.rs#L876), [`core/dr-pipeline/src/sidecar.rs:1336`](../../core/dr-pipeline/src/sidecar.rs#L1336), [`core/dr-pipeline/src/sidecar.rs:1515`](../../core/dr-pipeline/src/sidecar.rs#L1515), [`core/dr-pipeline/src/sidecar.rs:1604`](../../core/dr-pipeline/src/sidecar.rs#L1604), [`core/dr-pipeline/src/sidecar.rs:1708`](../../core/dr-pipeline/src/sidecar.rs#L1708), [`core/dr-pipeline/src/sidecar.rs:1916`](../../core/dr-pipeline/src/sidecar.rs#L1916), [`core/dr-pipeline/tests/mask_sidecar.rs:1157`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1157), [`core/dr-pipeline/tests/mask_sidecar.rs:1186`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1186), [`core/dr-pipeline/tests/mask_sidecar.rs:1204`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1204), [`core/dr-pipeline/tests/mask_sidecar.rs:1234`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1234), [`ui/dr-ui/src/develop/mask_ops.rs:341`](../../ui/dr-ui/src/develop/mask_ops.rs#L341), [`ui/dr-ui/src/develop/mask_ops.rs:638`](../../ui/dr-ui/src/develop/mask_ops.rs#L638), [`ui/dr-ui/src/develop/mask_ops.rs:665`](../../ui/dr-ui/src/develop/mask_ops.rs#L665), [`ui/dr-ui/src/develop/mask_ops.rs:697`](../../ui/dr-ui/src/develop/mask_ops.rs#L697), [`ui/dr-ui/src/develop/mask_ops.rs:721`](../../ui/dr-ui/src/develop/mask_ops.rs#L721), [`ui/dr-ui/src/develop/mask_ops.rs:727`](../../ui/dr-ui/src/develop/mask_ops.rs#L727), [`ui/dr-ui/src/develop/mask_ops.rs:734`](../../ui/dr-ui/src/develop/mask_ops.rs#L734), [`ui/dr-ui/src/develop/mask_ops.rs:754`](../../ui/dr-ui/src/develop/mask_ops.rs#L754), [`ui/dr-ui/src/labels.rs:63`](../../ui/dr-ui/src/labels.rs#L63), [`ui/dr-ui/src/masks_ui.rs:1216`](../../ui/dr-ui/src/masks_ui.rs#L1216), [`ui/dr-ui/src/masks_ui.rs:797`](../../ui/dr-ui/src/masks_ui.rs#L797) |
+| FR-DEV-10 | [`core/dr-gpu/src/mask.rs:1039`](../../core/dr-gpu/src/mask.rs#L1039), [`core/dr-gpu/src/mask.rs:1263`](../../core/dr-gpu/src/mask.rs#L1263), [`core/dr-gpu/src/mask.rs:1522`](../../core/dr-gpu/src/mask.rs#L1522), [`core/dr-gpu/src/mask.rs:422`](../../core/dr-gpu/src/mask.rs#L422), [`core/dr-gpu/src/mask.rs:464`](../../core/dr-gpu/src/mask.rs#L464), [`core/dr-gpu/src/mask.rs:58`](../../core/dr-gpu/src/mask.rs#L58), [`core/dr-gpu/src/mask.rs:60`](../../core/dr-gpu/src/mask.rs#L60), [`core/dr-gpu/src/mask.rs:846`](../../core/dr-gpu/src/mask.rs#L846), [`core/dr-gpu/src/mask.rs:85`](../../core/dr-gpu/src/mask.rs#L85), [`core/dr-gpu/src/mask.rs:925`](../../core/dr-gpu/src/mask.rs#L925), [`core/dr-gpu/src/shaders/mask.wgsl:117`](../../core/dr-gpu/src/shaders/mask.wgsl#L117), [`core/dr-gpu/src/shaders/mask.wgsl:59`](../../core/dr-gpu/src/shaders/mask.wgsl#L59), [`core/dr-gpu/tests/range_masks.rs:119`](../../core/dr-gpu/tests/range_masks.rs#L119), [`core/dr-gpu/tests/range_masks.rs:147`](../../core/dr-gpu/tests/range_masks.rs#L147), [`core/dr-gpu/tests/range_masks.rs:175`](../../core/dr-gpu/tests/range_masks.rs#L175), [`core/dr-gpu/tests/range_masks.rs:1`](../../core/dr-gpu/tests/range_masks.rs#L1), [`core/dr-gpu/tests/range_masks.rs:94`](../../core/dr-gpu/tests/range_masks.rs#L94), [`core/dr-pipeline/src/mask.rs:125`](../../core/dr-pipeline/src/mask.rs#L125), [`core/dr-pipeline/src/mask.rs:2890`](../../core/dr-pipeline/src/mask.rs#L2890), [`core/dr-pipeline/src/mask.rs:2903`](../../core/dr-pipeline/src/mask.rs#L2903), [`core/dr-pipeline/src/mask.rs:2922`](../../core/dr-pipeline/src/mask.rs#L2922), [`core/dr-pipeline/src/mask.rs:2936`](../../core/dr-pipeline/src/mask.rs#L2936), [`core/dr-pipeline/src/mask.rs:2946`](../../core/dr-pipeline/src/mask.rs#L2946), [`core/dr-pipeline/src/mask.rs:710`](../../core/dr-pipeline/src/mask.rs#L710), [`core/dr-pipeline/src/mask.rs:749`](../../core/dr-pipeline/src/mask.rs#L749), [`core/dr-pipeline/src/mask.rs:809`](../../core/dr-pipeline/src/mask.rs#L809), [`core/dr-pipeline/src/mask.rs:828`](../../core/dr-pipeline/src/mask.rs#L828), [`core/dr-pipeline/src/mask.rs:852`](../../core/dr-pipeline/src/mask.rs#L852), [`core/dr-pipeline/src/mask.rs:864`](../../core/dr-pipeline/src/mask.rs#L864), [`core/dr-pipeline/src/mask.rs:876`](../../core/dr-pipeline/src/mask.rs#L876), [`core/dr-pipeline/src/sidecar.rs:1336`](../../core/dr-pipeline/src/sidecar.rs#L1336), [`core/dr-pipeline/src/sidecar.rs:1515`](../../core/dr-pipeline/src/sidecar.rs#L1515), [`core/dr-pipeline/src/sidecar.rs:1604`](../../core/dr-pipeline/src/sidecar.rs#L1604), [`core/dr-pipeline/src/sidecar.rs:1708`](../../core/dr-pipeline/src/sidecar.rs#L1708), [`core/dr-pipeline/src/sidecar.rs:1916`](../../core/dr-pipeline/src/sidecar.rs#L1916), [`core/dr-pipeline/tests/mask_sidecar.rs:1157`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1157), [`core/dr-pipeline/tests/mask_sidecar.rs:1186`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1186), [`core/dr-pipeline/tests/mask_sidecar.rs:1204`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1204), [`core/dr-pipeline/tests/mask_sidecar.rs:1234`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1234), [`ui/dr-ui/src/develop/mask_ops.rs:341`](../../ui/dr-ui/src/develop/mask_ops.rs#L341), [`ui/dr-ui/src/develop/mask_ops.rs:638`](../../ui/dr-ui/src/develop/mask_ops.rs#L638), [`ui/dr-ui/src/develop/mask_ops.rs:665`](../../ui/dr-ui/src/develop/mask_ops.rs#L665), [`ui/dr-ui/src/develop/mask_ops.rs:697`](../../ui/dr-ui/src/develop/mask_ops.rs#L697), [`ui/dr-ui/src/develop/mask_ops.rs:721`](../../ui/dr-ui/src/develop/mask_ops.rs#L721), [`ui/dr-ui/src/develop/mask_ops.rs:727`](../../ui/dr-ui/src/develop/mask_ops.rs#L727), [`ui/dr-ui/src/develop/mask_ops.rs:734`](../../ui/dr-ui/src/develop/mask_ops.rs#L734), [`ui/dr-ui/src/develop/mask_ops.rs:754`](../../ui/dr-ui/src/develop/mask_ops.rs#L754), [`ui/dr-ui/src/labels.rs:63`](../../ui/dr-ui/src/labels.rs#L63), [`ui/dr-ui/src/masks_ui.rs:1216`](../../ui/dr-ui/src/masks_ui.rs#L1216), [`ui/dr-ui/src/masks_ui.rs:797`](../../ui/dr-ui/src/masks_ui.rs#L797) |
| FR-DEV-12 | [`core/dr-pipeline/ops/colour_grading.yaml:1`](../../core/dr-pipeline/ops/colour_grading.yaml#L1) |
| FR-DEV-16 | [`ui/dr-ui/src/lib.rs:426`](../../ui/dr-ui/src/lib.rs#L426), [`ui/dr-ui/src/masks_ui.rs:713`](../../ui/dr-ui/src/masks_ui.rs#L713), [`ui/dr-ui/ui/adjust.slint:940`](../../ui/dr-ui/ui/adjust.slint#L940), [`ui/dr-ui/ui/app.slint:2332`](../../ui/dr-ui/ui/app.slint#L2332), [`ui/dr-ui/ui/app.slint:2345`](../../ui/dr-ui/ui/app.slint#L2345), [`ui/dr-ui/ui/masks.slint:832`](../../ui/dr-ui/ui/masks.slint#L832) |
| FR-DEV-18 | [`core/dr-pipeline/src/ops/dehaze.rs:1`](../../core/dr-pipeline/src/ops/dehaze.rs#L1), [`core/dr-pipeline/src/ops/dehaze.rs:221`](../../core/dr-pipeline/src/ops/dehaze.rs#L221) |
-| FR-DEV-19 | [`core/dr-pipeline/src/mask.rs:1307`](../../core/dr-pipeline/src/mask.rs#L1307) |
-| FR-DEV-19a | [`core/dr-gpu/src/mask.rs:761`](../../core/dr-gpu/src/mask.rs#L761), [`core/dr-pipeline/src/mask.rs:1588`](../../core/dr-pipeline/src/mask.rs#L1588), [`core/dr-pipeline/src/mask.rs:2919`](../../core/dr-pipeline/src/mask.rs#L2919), [`core/dr-pipeline/src/mask.rs:988`](../../core/dr-pipeline/src/mask.rs#L988), [`core/dr-pipeline/src/sidecar.rs:1226`](../../core/dr-pipeline/src/sidecar.rs#L1226), [`core/dr-pipeline/tests/mask_sidecar.rs:1347`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1347), [`core/dr-pipeline/tests/mask_sidecar.rs:1373`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1373), [`ui/dr-ui/src/develop/masks.rs:431`](../../ui/dr-ui/src/develop/masks.rs#L431), [`ui/dr-ui/src/develop/masks.rs:492`](../../ui/dr-ui/src/develop/masks.rs#L492), [`ui/dr-ui/src/develop/masks.rs:511`](../../ui/dr-ui/src/develop/masks.rs#L511), [`ui/dr-ui/src/develop/session.rs:188`](../../ui/dr-ui/src/develop/session.rs#L188), [`ui/dr-ui/src/labels.rs:73`](../../ui/dr-ui/src/labels.rs#L73), [`ui/dr-ui/src/masks_ui.rs:1083`](../../ui/dr-ui/src/masks_ui.rs#L1083), [`ui/dr-ui/src/masks_ui.rs:1155`](../../ui/dr-ui/src/masks_ui.rs#L1155), [`ui/dr-ui/src/masks_ui.rs:1514`](../../ui/dr-ui/src/masks_ui.rs#L1514), [`ui/dr-ui/src/masks_ui.rs:270`](../../ui/dr-ui/src/masks_ui.rs#L270), [`ui/dr-ui/ui/masks.slint:150`](../../ui/dr-ui/ui/masks.slint#L150), [`ui/dr-ui/ui/masks.slint:175`](../../ui/dr-ui/ui/masks.slint#L175), [`ui/dr-ui/ui/masks.slint:337`](../../ui/dr-ui/ui/masks.slint#L337), [`ui/dr-ui/ui/masks.slint:406`](../../ui/dr-ui/ui/masks.slint#L406), [`ui/dr-ui/ui/masks.slint:770`](../../ui/dr-ui/ui/masks.slint#L770), [`ui/dr-ui/ui/masks.slint:855`](../../ui/dr-ui/ui/masks.slint#L855) |
+| FR-DEV-19 | [`core/dr-pipeline/src/mask.rs:1341`](../../core/dr-pipeline/src/mask.rs#L1341) |
+| FR-DEV-19a | [`core/dr-gpu/src/mask.rs:655`](../../core/dr-gpu/src/mask.rs#L655), [`core/dr-gpu/src/mask.rs:773`](../../core/dr-gpu/src/mask.rs#L773), [`core/dr-gpu/tests/local_adjustments.rs:793`](../../core/dr-gpu/tests/local_adjustments.rs#L793), [`core/dr-gpu/tests/local_adjustments.rs:841`](../../core/dr-gpu/tests/local_adjustments.rs#L841), [`core/dr-pipeline/src/mask.rs:1022`](../../core/dr-pipeline/src/mask.rs#L1022), [`core/dr-pipeline/src/mask.rs:1633`](../../core/dr-pipeline/src/mask.rs#L1633), [`core/dr-pipeline/src/mask.rs:2964`](../../core/dr-pipeline/src/mask.rs#L2964), [`core/dr-pipeline/src/mask.rs:3085`](../../core/dr-pipeline/src/mask.rs#L3085), [`core/dr-pipeline/src/mask.rs:3120`](../../core/dr-pipeline/src/mask.rs#L3120), [`core/dr-pipeline/src/mask.rs:929`](../../core/dr-pipeline/src/mask.rs#L929), [`core/dr-pipeline/src/mask.rs:964`](../../core/dr-pipeline/src/mask.rs#L964), [`core/dr-pipeline/src/sidecar.rs:1226`](../../core/dr-pipeline/src/sidecar.rs#L1226), [`core/dr-pipeline/tests/mask_sidecar.rs:1347`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1347), [`core/dr-pipeline/tests/mask_sidecar.rs:1369`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1369), [`core/dr-pipeline/tests/mask_sidecar.rs:1395`](../../core/dr-pipeline/tests/mask_sidecar.rs#L1395), [`ui/dr-ui/src/develop/masks.rs:431`](../../ui/dr-ui/src/develop/masks.rs#L431), [`ui/dr-ui/src/develop/masks.rs:492`](../../ui/dr-ui/src/develop/masks.rs#L492), [`ui/dr-ui/src/develop/masks.rs:511`](../../ui/dr-ui/src/develop/masks.rs#L511), [`ui/dr-ui/src/develop/session.rs:188`](../../ui/dr-ui/src/develop/session.rs#L188), [`ui/dr-ui/src/labels.rs:73`](../../ui/dr-ui/src/labels.rs#L73), [`ui/dr-ui/src/masks_ui.rs:1083`](../../ui/dr-ui/src/masks_ui.rs#L1083), [`ui/dr-ui/src/masks_ui.rs:1155`](../../ui/dr-ui/src/masks_ui.rs#L1155), [`ui/dr-ui/src/masks_ui.rs:1514`](../../ui/dr-ui/src/masks_ui.rs#L1514), [`ui/dr-ui/src/masks_ui.rs:270`](../../ui/dr-ui/src/masks_ui.rs#L270), [`ui/dr-ui/ui/masks.slint:150`](../../ui/dr-ui/ui/masks.slint#L150), [`ui/dr-ui/ui/masks.slint:175`](../../ui/dr-ui/ui/masks.slint#L175), [`ui/dr-ui/ui/masks.slint:337`](../../ui/dr-ui/ui/masks.slint#L337), [`ui/dr-ui/ui/masks.slint:406`](../../ui/dr-ui/ui/masks.slint#L406), [`ui/dr-ui/ui/masks.slint:770`](../../ui/dr-ui/ui/masks.slint#L770), [`ui/dr-ui/ui/masks.slint:855`](../../ui/dr-ui/ui/masks.slint#L855) |
| FR-DEV-19b | [`ui/dr-ui/src/develop/mask_ops.rs:606`](../../ui/dr-ui/src/develop/mask_ops.rs#L606), [`ui/dr-ui/src/develop/masks.rs:592`](../../ui/dr-ui/src/develop/masks.rs#L592), [`ui/dr-ui/src/develop/session.rs:197`](../../ui/dr-ui/src/develop/session.rs#L197), [`ui/dr-ui/src/develop/session.rs:204`](../../ui/dr-ui/src/develop/session.rs#L204), [`ui/dr-ui/src/labels.rs:67`](../../ui/dr-ui/src/labels.rs#L67), [`ui/dr-ui/src/masks_ui.rs:1188`](../../ui/dr-ui/src/masks_ui.rs#L1188), [`ui/dr-ui/src/masks_ui.rs:1407`](../../ui/dr-ui/src/masks_ui.rs#L1407), [`ui/dr-ui/src/masks_ui.rs:1435`](../../ui/dr-ui/src/masks_ui.rs#L1435), [`ui/dr-ui/src/masks_ui.rs:1778`](../../ui/dr-ui/src/masks_ui.rs#L1778), [`ui/dr-ui/src/masks_ui.rs:908`](../../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/ui/app.slint:1907`](../../ui/dr-ui/ui/app.slint#L1907), [`ui/dr-ui/ui/app.slint:737`](../../ui/dr-ui/ui/app.slint#L737), [`ui/dr-ui/ui/masks.slint:1126`](../../ui/dr-ui/ui/masks.slint#L1126), [`ui/dr-ui/ui/masks.slint:453`](../../ui/dr-ui/ui/masks.slint#L453), [`ui/dr-ui/ui/masks.slint:774`](../../ui/dr-ui/ui/masks.slint#L774), [`ui/dr-ui/ui/masks.slint:848`](../../ui/dr-ui/ui/masks.slint#L848), [`ui/dr-ui/ui/masks.slint:864`](../../ui/dr-ui/ui/masks.slint#L864) |
-| FR-DEV-19c | [`core/dr-gpu/src/mask.rs:715`](../../core/dr-gpu/src/mask.rs#L715), [`core/dr-gpu/tests/local_adjustments.rs:830`](../../core/dr-gpu/tests/local_adjustments.rs#L830), [`core/dr-gpu/tests/local_adjustments.rs:887`](../../core/dr-gpu/tests/local_adjustments.rs#L887), [`core/dr-gpu/tests/local_adjustments.rs:922`](../../core/dr-gpu/tests/local_adjustments.rs#L922), [`core/dr-gpu/tests/local_adjustments.rs:967`](../../core/dr-gpu/tests/local_adjustments.rs#L967), [`core/dr-pipeline/src/graph.rs:893`](../../core/dr-pipeline/src/graph.rs#L893), [`core/dr-pipeline/src/mask.rs:1796`](../../core/dr-pipeline/src/mask.rs#L1796), [`core/dr-pipeline/src/mask.rs:1822`](../../core/dr-pipeline/src/mask.rs#L1822), [`core/dr-pipeline/src/mask.rs:1837`](../../core/dr-pipeline/src/mask.rs#L1837), [`core/dr-pipeline/src/mask.rs:1957`](../../core/dr-pipeline/src/mask.rs#L1957), [`core/dr-pipeline/src/mask.rs:2005`](../../core/dr-pipeline/src/mask.rs#L2005), [`core/dr-pipeline/src/mask.rs:2149`](../../core/dr-pipeline/src/mask.rs#L2149), [`core/dr-pipeline/src/mask.rs:2386`](../../core/dr-pipeline/src/mask.rs#L2386), [`core/dr-pipeline/src/operation.rs:1748`](../../core/dr-pipeline/src/operation.rs#L1748), [`core/dr-pipeline/src/operation.rs:588`](../../core/dr-pipeline/src/operation.rs#L588), [`core/dr-pipeline/src/operation.rs:863`](../../core/dr-pipeline/src/operation.rs#L863), [`ui/dr-ui/src/develop/mask_ops.rs:1199`](../../ui/dr-ui/src/develop/mask_ops.rs#L1199), [`ui/dr-ui/src/develop/mask_ops.rs:1235`](../../ui/dr-ui/src/develop/mask_ops.rs#L1235), [`ui/dr-ui/src/develop/mask_ops.rs:318`](../../ui/dr-ui/src/develop/mask_ops.rs#L318), [`ui/dr-ui/src/develop/mask_ops.rs:58`](../../ui/dr-ui/src/develop/mask_ops.rs#L58), [`ui/dr-ui/src/develop/masks.rs:12`](../../ui/dr-ui/src/develop/masks.rs#L12), [`ui/dr-ui/src/develop/masks.rs:176`](../../ui/dr-ui/src/develop/masks.rs#L176), [`ui/dr-ui/src/develop/masks.rs:21`](../../ui/dr-ui/src/develop/masks.rs#L21), [`ui/dr-ui/src/develop/masks.rs:43`](../../ui/dr-ui/src/develop/masks.rs#L43), [`ui/dr-ui/src/develop/render.rs:260`](../../ui/dr-ui/src/develop/render.rs#L260), [`ui/dr-ui/src/develop/session.rs:223`](../../ui/dr-ui/src/develop/session.rs#L223), [`ui/dr-ui/src/develop/session.rs:232`](../../ui/dr-ui/src/develop/session.rs#L232), [`ui/dr-ui/src/masks_ui.rs:1024`](../../ui/dr-ui/src/masks_ui.rs#L1024), [`ui/dr-ui/src/masks_ui.rs:1407`](../../ui/dr-ui/src/masks_ui.rs#L1407), [`ui/dr-ui/src/masks_ui.rs:152`](../../ui/dr-ui/src/masks_ui.rs#L152), [`ui/dr-ui/src/masks_ui.rs:1778`](../../ui/dr-ui/src/masks_ui.rs#L1778), [`ui/dr-ui/src/masks_ui.rs:291`](../../ui/dr-ui/src/masks_ui.rs#L291), [`ui/dr-ui/src/masks_ui.rs:317`](../../ui/dr-ui/src/masks_ui.rs#L317), [`ui/dr-ui/src/masks_ui.rs:677`](../../ui/dr-ui/src/masks_ui.rs#L677), [`ui/dr-ui/src/masks_ui.rs:996`](../../ui/dr-ui/src/masks_ui.rs#L996), [`ui/dr-ui/ui/icons.slint:339`](../../ui/dr-ui/ui/icons.slint#L339), [`ui/dr-ui/ui/masks.slint:1091`](../../ui/dr-ui/ui/masks.slint#L1091), [`ui/dr-ui/ui/masks.slint:183`](../../ui/dr-ui/ui/masks.slint#L183), [`ui/dr-ui/ui/masks.slint:292`](../../ui/dr-ui/ui/masks.slint#L292), [`ui/dr-ui/ui/masks.slint:517`](../../ui/dr-ui/ui/masks.slint#L517), [`ui/dr-ui/ui/masks.slint:70`](../../ui/dr-ui/ui/masks.slint#L70), [`ui/dr-ui/ui/masks.slint:782`](../../ui/dr-ui/ui/masks.slint#L782), [`ui/dr-ui/ui/masks.slint:850`](../../ui/dr-ui/ui/masks.slint#L850), [`ui/dr-ui/ui/masks.slint:938`](../../ui/dr-ui/ui/masks.slint#L938) |
+| FR-DEV-19c | [`core/dr-gpu/src/mask.rs:727`](../../core/dr-gpu/src/mask.rs#L727), [`core/dr-gpu/tests/local_adjustments.rs:1000`](../../core/dr-gpu/tests/local_adjustments.rs#L1000), [`core/dr-gpu/tests/local_adjustments.rs:1035`](../../core/dr-gpu/tests/local_adjustments.rs#L1035), [`core/dr-gpu/tests/local_adjustments.rs:1080`](../../core/dr-gpu/tests/local_adjustments.rs#L1080), [`core/dr-gpu/tests/local_adjustments.rs:943`](../../core/dr-gpu/tests/local_adjustments.rs#L943), [`core/dr-pipeline/src/graph.rs:893`](../../core/dr-pipeline/src/graph.rs#L893), [`core/dr-pipeline/src/mask.rs:1841`](../../core/dr-pipeline/src/mask.rs#L1841), [`core/dr-pipeline/src/mask.rs:1867`](../../core/dr-pipeline/src/mask.rs#L1867), [`core/dr-pipeline/src/mask.rs:1882`](../../core/dr-pipeline/src/mask.rs#L1882), [`core/dr-pipeline/src/mask.rs:2002`](../../core/dr-pipeline/src/mask.rs#L2002), [`core/dr-pipeline/src/mask.rs:2050`](../../core/dr-pipeline/src/mask.rs#L2050), [`core/dr-pipeline/src/mask.rs:2194`](../../core/dr-pipeline/src/mask.rs#L2194), [`core/dr-pipeline/src/mask.rs:2431`](../../core/dr-pipeline/src/mask.rs#L2431), [`core/dr-pipeline/src/operation.rs:1748`](../../core/dr-pipeline/src/operation.rs#L1748), [`core/dr-pipeline/src/operation.rs:588`](../../core/dr-pipeline/src/operation.rs#L588), [`core/dr-pipeline/src/operation.rs:863`](../../core/dr-pipeline/src/operation.rs#L863), [`ui/dr-ui/src/develop/mask_ops.rs:1199`](../../ui/dr-ui/src/develop/mask_ops.rs#L1199), [`ui/dr-ui/src/develop/mask_ops.rs:1235`](../../ui/dr-ui/src/develop/mask_ops.rs#L1235), [`ui/dr-ui/src/develop/mask_ops.rs:318`](../../ui/dr-ui/src/develop/mask_ops.rs#L318), [`ui/dr-ui/src/develop/mask_ops.rs:58`](../../ui/dr-ui/src/develop/mask_ops.rs#L58), [`ui/dr-ui/src/develop/masks.rs:12`](../../ui/dr-ui/src/develop/masks.rs#L12), [`ui/dr-ui/src/develop/masks.rs:176`](../../ui/dr-ui/src/develop/masks.rs#L176), [`ui/dr-ui/src/develop/masks.rs:21`](../../ui/dr-ui/src/develop/masks.rs#L21), [`ui/dr-ui/src/develop/masks.rs:43`](../../ui/dr-ui/src/develop/masks.rs#L43), [`ui/dr-ui/src/develop/render.rs:260`](../../ui/dr-ui/src/develop/render.rs#L260), [`ui/dr-ui/src/develop/session.rs:223`](../../ui/dr-ui/src/develop/session.rs#L223), [`ui/dr-ui/src/develop/session.rs:232`](../../ui/dr-ui/src/develop/session.rs#L232), [`ui/dr-ui/src/masks_ui.rs:1024`](../../ui/dr-ui/src/masks_ui.rs#L1024), [`ui/dr-ui/src/masks_ui.rs:1407`](../../ui/dr-ui/src/masks_ui.rs#L1407), [`ui/dr-ui/src/masks_ui.rs:152`](../../ui/dr-ui/src/masks_ui.rs#L152), [`ui/dr-ui/src/masks_ui.rs:1778`](../../ui/dr-ui/src/masks_ui.rs#L1778), [`ui/dr-ui/src/masks_ui.rs:291`](../../ui/dr-ui/src/masks_ui.rs#L291), [`ui/dr-ui/src/masks_ui.rs:317`](../../ui/dr-ui/src/masks_ui.rs#L317), [`ui/dr-ui/src/masks_ui.rs:677`](../../ui/dr-ui/src/masks_ui.rs#L677), [`ui/dr-ui/src/masks_ui.rs:996`](../../ui/dr-ui/src/masks_ui.rs#L996), [`ui/dr-ui/ui/icons.slint:339`](../../ui/dr-ui/ui/icons.slint#L339), [`ui/dr-ui/ui/masks.slint:1091`](../../ui/dr-ui/ui/masks.slint#L1091), [`ui/dr-ui/ui/masks.slint:183`](../../ui/dr-ui/ui/masks.slint#L183), [`ui/dr-ui/ui/masks.slint:292`](../../ui/dr-ui/ui/masks.slint#L292), [`ui/dr-ui/ui/masks.slint:517`](../../ui/dr-ui/ui/masks.slint#L517), [`ui/dr-ui/ui/masks.slint:70`](../../ui/dr-ui/ui/masks.slint#L70), [`ui/dr-ui/ui/masks.slint:782`](../../ui/dr-ui/ui/masks.slint#L782), [`ui/dr-ui/ui/masks.slint:850`](../../ui/dr-ui/ui/masks.slint#L850), [`ui/dr-ui/ui/masks.slint:938`](../../ui/dr-ui/ui/masks.slint#L938) |
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:403`](../../core/dr-pipeline/src/operation.rs#L403) |
| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2443`](../../core/dr-gpu/src/adjust.rs#L2443), [`core/dr-gpu/src/adjust.rs:679`](../../core/dr-gpu/src/adjust.rs#L679), [`core/dr-gpu/src/adjust.rs:798`](../../core/dr-gpu/src/adjust.rs#L798), [`core/dr-gpu/src/adjust.rs:84`](../../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/tone_curve.rs:1`](../../core/dr-gpu/tests/tone_curve.rs#L1), [`core/dr-pipeline/src/coverage.rs:122`](../../core/dr-pipeline/src/coverage.rs#L122), [`core/dr-pipeline/src/coverage.rs:1`](../../core/dr-pipeline/src/coverage.rs#L1), [`core/dr-pipeline/src/detail.rs:434`](../../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:524`](../../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/framing.rs:177`](../../core/dr-pipeline/src/framing.rs#L177), [`core/dr-pipeline/src/framing.rs:234`](../../core/dr-pipeline/src/framing.rs#L234), [`core/dr-pipeline/src/framing.rs:314`](../../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/framing.rs:498`](../../core/dr-pipeline/src/framing.rs#L498), [`core/dr-pipeline/src/framing.rs:753`](../../core/dr-pipeline/src/framing.rs#L753), [`core/dr-pipeline/src/graph.rs:1348`](../../core/dr-pipeline/src/graph.rs#L1348), [`core/dr-pipeline/src/graph.rs:1379`](../../core/dr-pipeline/src/graph.rs#L1379), [`core/dr-pipeline/src/graph.rs:1402`](../../core/dr-pipeline/src/graph.rs#L1402), [`core/dr-pipeline/src/graph.rs:1440`](../../core/dr-pipeline/src/graph.rs#L1440), [`core/dr-pipeline/src/graph.rs:1471`](../../core/dr-pipeline/src/graph.rs#L1471), [`core/dr-pipeline/src/graph.rs:225`](../../core/dr-pipeline/src/graph.rs#L225), [`core/dr-pipeline/src/graph.rs:306`](../../core/dr-pipeline/src/graph.rs#L306), [`core/dr-pipeline/src/graph.rs:344`](../../core/dr-pipeline/src/graph.rs#L344), [`core/dr-pipeline/src/graph.rs:350`](../../core/dr-pipeline/src/graph.rs#L350), [`core/dr-pipeline/src/graph.rs:860`](../../core/dr-pipeline/src/graph.rs#L860), [`core/dr-pipeline/src/graph.rs:952`](../../core/dr-pipeline/src/graph.rs#L952), [`core/dr-pipeline/src/lens.rs:93`](../../core/dr-pipeline/src/lens.rs#L93), [`core/dr-pipeline/src/mask.rs:205`](../../core/dr-pipeline/src/mask.rs#L205), [`core/dr-pipeline/src/mask.rs:558`](../../core/dr-pipeline/src/mask.rs#L558), [`core/dr-pipeline/src/neutral.rs:1`](../../core/dr-pipeline/src/neutral.rs#L1), [`core/dr-pipeline/src/neutral.rs:306`](../../core/dr-pipeline/src/neutral.rs#L306), [`core/dr-pipeline/src/neutral.rs:322`](../../core/dr-pipeline/src/neutral.rs#L322), [`core/dr-pipeline/src/neutral.rs:338`](../../core/dr-pipeline/src/neutral.rs#L338), [`core/dr-pipeline/src/neutral.rs:360`](../../core/dr-pipeline/src/neutral.rs#L360), [`core/dr-pipeline/src/neutral.rs:81`](../../core/dr-pipeline/src/neutral.rs#L81), [`core/dr-pipeline/src/operation.rs:330`](../../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:565`](../../core/dr-pipeline/src/operation.rs#L565), [`core/dr-pipeline/src/operation.rs:637`](../../core/dr-pipeline/src/operation.rs#L637), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:210`](../../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/preset.rs:751`](../../core/dr-pipeline/src/preset.rs#L751), [`core/dr-pipeline/src/sidecar.rs:1400`](../../core/dr-pipeline/src/sidecar.rs#L1400), [`core/dr-pipeline/src/sidecar.rs:192`](../../core/dr-pipeline/src/sidecar.rs#L192), [`core/dr-pipeline/src/sidecar.rs:2232`](../../core/dr-pipeline/src/sidecar.rs#L2232), [`core/dr-pipeline/src/sidecar.rs:2312`](../../core/dr-pipeline/src/sidecar.rs#L2312), [`core/dr-pipeline/tests/tone_curve.rs:1`](../../core/dr-pipeline/tests/tone_curve.rs#L1), [`core/dr-segment/src/refine.rs:937`](../../core/dr-segment/src/refine.rs#L937), [`ui/dr-ui/src/develop/curves.rs:163`](../../ui/dr-ui/src/develop/curves.rs#L163), [`ui/dr-ui/src/develop/framing.rs:10`](../../ui/dr-ui/src/develop/framing.rs#L10), [`ui/dr-ui/src/develop/framing.rs:183`](../../ui/dr-ui/src/develop/framing.rs#L183), [`ui/dr-ui/src/develop/framing.rs:213`](../../ui/dr-ui/src/develop/framing.rs#L213), [`ui/dr-ui/src/develop/framing.rs:279`](../../ui/dr-ui/src/develop/framing.rs#L279), [`ui/dr-ui/src/develop/framing.rs:293`](../../ui/dr-ui/src/develop/framing.rs#L293), [`ui/dr-ui/src/develop/framing.rs:495`](../../ui/dr-ui/src/develop/framing.rs#L495), [`ui/dr-ui/src/develop/framing.rs:71`](../../ui/dr-ui/src/develop/framing.rs#L71), [`ui/dr-ui/src/develop/mask_ops.rs:1076`](../../ui/dr-ui/src/develop/mask_ops.rs#L1076), [`ui/dr-ui/src/develop/mask_ops.rs:1340`](../../ui/dr-ui/src/develop/mask_ops.rs#L1340), [`ui/dr-ui/src/develop/mask_ops.rs:178`](../../ui/dr-ui/src/develop/mask_ops.rs#L178), [`ui/dr-ui/src/develop/mask_ops.rs:969`](../../ui/dr-ui/src/develop/mask_ops.rs#L969), [`ui/dr-ui/src/develop/masks.rs:262`](../../ui/dr-ui/src/develop/masks.rs#L262), [`ui/dr-ui/src/develop/masks.rs:360`](../../ui/dr-ui/src/develop/masks.rs#L360), [`ui/dr-ui/src/develop/masks.rs:740`](../../ui/dr-ui/src/develop/masks.rs#L740), [`ui/dr-ui/src/develop/masks.rs:770`](../../ui/dr-ui/src/develop/masks.rs#L770), [`ui/dr-ui/src/develop/render.rs:1240`](../../ui/dr-ui/src/develop/render.rs#L1240), [`ui/dr-ui/src/develop/rows.rs:106`](../../ui/dr-ui/src/develop/rows.rs#L106), [`ui/dr-ui/src/develop/rows.rs:195`](../../ui/dr-ui/src/develop/rows.rs#L195), [`ui/dr-ui/src/develop/rows.rs:231`](../../ui/dr-ui/src/develop/rows.rs#L231), [`ui/dr-ui/src/develop/rows.rs:679`](../../ui/dr-ui/src/develop/rows.rs#L679), [`ui/dr-ui/src/develop/segmentation.rs:139`](../../ui/dr-ui/src/develop/segmentation.rs#L139), [`ui/dr-ui/src/develop/segmentation.rs:302`](../../ui/dr-ui/src/develop/segmentation.rs#L302), [`ui/dr-ui/src/develop/segmentation.rs:339`](../../ui/dr-ui/src/develop/segmentation.rs#L339), [`ui/dr-ui/src/develop/segmentation.rs:572`](../../ui/dr-ui/src/develop/segmentation.rs#L572), [`ui/dr-ui/src/develop/segmentation.rs:590`](../../ui/dr-ui/src/develop/segmentation.rs#L590), [`ui/dr-ui/src/develop/segmentation.rs:604`](../../ui/dr-ui/src/develop/segmentation.rs#L604), [`ui/dr-ui/src/develop/segmentation.rs:626`](../../ui/dr-ui/src/develop/segmentation.rs#L626), [`ui/dr-ui/src/develop/segmentation.rs:77`](../../ui/dr-ui/src/develop/segmentation.rs#L77), [`ui/dr-ui/src/develop/segmentation.rs:806`](../../ui/dr-ui/src/develop/segmentation.rs#L806), [`ui/dr-ui/src/develop/session.rs:158`](../../ui/dr-ui/src/develop/session.rs#L158), [`ui/dr-ui/src/develop/session.rs:251`](../../ui/dr-ui/src/develop/session.rs#L251), [`ui/dr-ui/src/develop/session.rs:280`](../../ui/dr-ui/src/develop/session.rs#L280), [`ui/dr-ui/src/develop/session.rs:429`](../../ui/dr-ui/src/develop/session.rs#L429), [`ui/dr-ui/src/develop/session.rs:489`](../../ui/dr-ui/src/develop/session.rs#L489), [`ui/dr-ui/src/develop/session.rs:561`](../../ui/dr-ui/src/develop/session.rs#L561), [`ui/dr-ui/src/develop/session.rs:711`](../../ui/dr-ui/src/develop/session.rs#L711), [`ui/dr-ui/src/develop/white_balance.rs:11`](../../ui/dr-ui/src/develop/white_balance.rs#L11), [`ui/dr-ui/src/develop/white_balance.rs:162`](../../ui/dr-ui/src/develop/white_balance.rs#L162), [`ui/dr-ui/src/develop/white_balance.rs:193`](../../ui/dr-ui/src/develop/white_balance.rs#L193), [`ui/dr-ui/src/develop/white_balance.rs:276`](../../ui/dr-ui/src/develop/white_balance.rs#L276), [`ui/dr-ui/src/develop/white_balance.rs:338`](../../ui/dr-ui/src/develop/white_balance.rs#L338), [`ui/dr-ui/src/develop_ui.rs:1048`](../../ui/dr-ui/src/develop_ui.rs#L1048), [`ui/dr-ui/src/develop_ui.rs:1102`](../../ui/dr-ui/src/develop_ui.rs#L1102), [`ui/dr-ui/src/develop_ui.rs:117`](../../ui/dr-ui/src/develop_ui.rs#L117), [`ui/dr-ui/src/develop_ui.rs:414`](../../ui/dr-ui/src/develop_ui.rs#L414), [`ui/dr-ui/src/develop_ui.rs:634`](../../ui/dr-ui/src/develop_ui.rs#L634), [`ui/dr-ui/src/develop_ui.rs:972`](../../ui/dr-ui/src/develop_ui.rs#L972), [`ui/dr-ui/src/labels.rs:35`](../../ui/dr-ui/src/labels.rs#L35), [`ui/dr-ui/src/lib.rs:2197`](../../ui/dr-ui/src/lib.rs#L2197), [`ui/dr-ui/src/lib.rs:421`](../../ui/dr-ui/src/lib.rs#L421), [`ui/dr-ui/src/lib.rs:450`](../../ui/dr-ui/src/lib.rs#L450), [`ui/dr-ui/src/lib.rs:556`](../../ui/dr-ui/src/lib.rs#L556), [`ui/dr-ui/src/lib.rs:580`](../../ui/dr-ui/src/lib.rs#L580), [`ui/dr-ui/src/library/sidecar.rs:63`](../../ui/dr-ui/src/library/sidecar.rs#L63), [`ui/dr-ui/src/masks_ui.rs:1537`](../../ui/dr-ui/src/masks_ui.rs#L1537), [`ui/dr-ui/src/masks_ui.rs:1651`](../../ui/dr-ui/src/masks_ui.rs#L1651), [`ui/dr-ui/src/masks_ui.rs:339`](../../ui/dr-ui/src/masks_ui.rs#L339), [`ui/dr-ui/src/masks_ui.rs:44`](../../ui/dr-ui/src/masks_ui.rs#L44), [`ui/dr-ui/src/presets.rs:414`](../../ui/dr-ui/src/presets.rs#L414), [`ui/dr-ui/src/segmentation.rs:117`](../../ui/dr-ui/src/segmentation.rs#L117), [`ui/dr-ui/src/segmentation.rs:353`](../../ui/dr-ui/src/segmentation.rs#L353), [`ui/dr-ui/src/segmentation.rs:369`](../../ui/dr-ui/src/segmentation.rs#L369), [`ui/dr-ui/src/segmentation.rs:499`](../../ui/dr-ui/src/segmentation.rs#L499), [`ui/dr-ui/src/segmentation.rs:527`](../../ui/dr-ui/src/segmentation.rs#L527), [`ui/dr-ui/ui/adjust.slint:126`](../../ui/dr-ui/ui/adjust.slint#L126), [`ui/dr-ui/ui/adjust.slint:1438`](../../ui/dr-ui/ui/adjust.slint#L1438), [`ui/dr-ui/ui/adjust.slint:171`](../../ui/dr-ui/ui/adjust.slint#L171), [`ui/dr-ui/ui/adjust.slint:46`](../../ui/dr-ui/ui/adjust.slint#L46), [`ui/dr-ui/ui/adjust.slint:624`](../../ui/dr-ui/ui/adjust.slint#L624), [`ui/dr-ui/ui/adjust.slint:650`](../../ui/dr-ui/ui/adjust.slint#L650), [`ui/dr-ui/ui/adjust.slint:669`](../../ui/dr-ui/ui/adjust.slint#L669), [`ui/dr-ui/ui/adjust.slint:790`](../../ui/dr-ui/ui/adjust.slint#L790), [`ui/dr-ui/ui/adjust.slint:889`](../../ui/dr-ui/ui/adjust.slint#L889), [`ui/dr-ui/ui/adjust.slint:961`](../../ui/dr-ui/ui/adjust.slint#L961), [`ui/dr-ui/ui/app.slint:193`](../../ui/dr-ui/ui/app.slint#L193), [`ui/dr-ui/ui/app.slint:1972`](../../ui/dr-ui/ui/app.slint#L1972), [`ui/dr-ui/ui/app.slint:2031`](../../ui/dr-ui/ui/app.slint#L2031), [`ui/dr-ui/ui/develop.slint:261`](../../ui/dr-ui/ui/develop.slint#L261), [`ui/dr-ui/ui/masks.slint:1216`](../../ui/dr-ui/ui/masks.slint#L1216), [`ui/dr-ui/ui/masks.slint:753`](../../ui/dr-ui/ui/masks.slint#L753), [`ui/dr-ui/ui/masks.slint:812`](../../ui/dr-ui/ui/masks.slint#L812) |
-| FR-DEV-3a | [`core/dr-pipeline/ops/exposure.yaml:1`](../../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:212`](../../core/dr-pipeline/src/descriptor.rs#L212), [`core/dr-pipeline/src/descriptor.rs:252`](../../core/dr-pipeline/src/descriptor.rs#L252), [`core/dr-pipeline/src/descriptor.rs:276`](../../core/dr-pipeline/src/descriptor.rs#L276), [`core/dr-pipeline/src/descriptor.rs:331`](../../core/dr-pipeline/src/descriptor.rs#L331), [`core/dr-pipeline/src/framing.rs:395`](../../core/dr-pipeline/src/framing.rs#L395), [`core/dr-pipeline/src/graph.rs:25`](../../core/dr-pipeline/src/graph.rs#L25), [`core/dr-pipeline/src/graph.rs:393`](../../core/dr-pipeline/src/graph.rs#L393), [`core/dr-pipeline/src/graph.rs:47`](../../core/dr-pipeline/src/graph.rs#L47), [`core/dr-pipeline/src/graph.rs:534`](../../core/dr-pipeline/src/graph.rs#L534), [`core/dr-pipeline/src/graph.rs:60`](../../core/dr-pipeline/src/graph.rs#L60), [`core/dr-pipeline/src/mask.rs:1699`](../../core/dr-pipeline/src/mask.rs#L1699), [`core/dr-pipeline/src/neutral.rs:1`](../../core/dr-pipeline/src/neutral.rs#L1), [`core/dr-pipeline/src/neutral.rs:378`](../../core/dr-pipeline/src/neutral.rs#L378), [`core/dr-pipeline/src/operation.rs:232`](../../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/src/operation.rs:365`](../../core/dr-pipeline/src/operation.rs#L365), [`core/dr-pipeline/src/ops/curve.rs:319`](../../core/dr-pipeline/src/ops/curve.rs#L319), [`ui/dr-ui/src/develop/curves.rs:36`](../../ui/dr-ui/src/develop/curves.rs#L36), [`ui/dr-ui/src/develop/rows.rs:118`](../../ui/dr-ui/src/develop/rows.rs#L118), [`ui/dr-ui/src/develop/rows.rs:679`](../../ui/dr-ui/src/develop/rows.rs#L679), [`ui/dr-ui/src/develop/rows.rs:877`](../../ui/dr-ui/src/develop/rows.rs#L877), [`ui/dr-ui/src/lib.rs:881`](../../ui/dr-ui/src/lib.rs#L881), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../../ui/dr-ui/tests/ui_names_no_operation.rs#L1), [`ui/dr-ui/ui/app.slint:2278`](../../ui/dr-ui/ui/app.slint#L2278) |
+| FR-DEV-3a | [`core/dr-pipeline/ops/exposure.yaml:1`](../../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:212`](../../core/dr-pipeline/src/descriptor.rs#L212), [`core/dr-pipeline/src/descriptor.rs:252`](../../core/dr-pipeline/src/descriptor.rs#L252), [`core/dr-pipeline/src/descriptor.rs:276`](../../core/dr-pipeline/src/descriptor.rs#L276), [`core/dr-pipeline/src/descriptor.rs:331`](../../core/dr-pipeline/src/descriptor.rs#L331), [`core/dr-pipeline/src/framing.rs:395`](../../core/dr-pipeline/src/framing.rs#L395), [`core/dr-pipeline/src/graph.rs:25`](../../core/dr-pipeline/src/graph.rs#L25), [`core/dr-pipeline/src/graph.rs:393`](../../core/dr-pipeline/src/graph.rs#L393), [`core/dr-pipeline/src/graph.rs:47`](../../core/dr-pipeline/src/graph.rs#L47), [`core/dr-pipeline/src/graph.rs:534`](../../core/dr-pipeline/src/graph.rs#L534), [`core/dr-pipeline/src/graph.rs:60`](../../core/dr-pipeline/src/graph.rs#L60), [`core/dr-pipeline/src/mask.rs:1744`](../../core/dr-pipeline/src/mask.rs#L1744), [`core/dr-pipeline/src/neutral.rs:1`](../../core/dr-pipeline/src/neutral.rs#L1), [`core/dr-pipeline/src/neutral.rs:378`](../../core/dr-pipeline/src/neutral.rs#L378), [`core/dr-pipeline/src/operation.rs:232`](../../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/src/operation.rs:365`](../../core/dr-pipeline/src/operation.rs#L365), [`core/dr-pipeline/src/ops/curve.rs:319`](../../core/dr-pipeline/src/ops/curve.rs#L319), [`ui/dr-ui/src/develop/curves.rs:36`](../../ui/dr-ui/src/develop/curves.rs#L36), [`ui/dr-ui/src/develop/rows.rs:118`](../../ui/dr-ui/src/develop/rows.rs#L118), [`ui/dr-ui/src/develop/rows.rs:679`](../../ui/dr-ui/src/develop/rows.rs#L679), [`ui/dr-ui/src/develop/rows.rs:877`](../../ui/dr-ui/src/develop/rows.rs#L877), [`ui/dr-ui/src/lib.rs:881`](../../ui/dr-ui/src/lib.rs#L881), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../../ui/dr-ui/tests/ui_names_no_operation.rs#L1), [`ui/dr-ui/ui/app.slint:2278`](../../ui/dr-ui/ui/app.slint#L2278) |
| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:276`](../../core/dr-pipeline/src/descriptor.rs#L276), [`core/dr-pipeline/src/framing.rs:395`](../../core/dr-pipeline/src/framing.rs#L395), [`core/dr-pipeline/src/graph.rs:60`](../../core/dr-pipeline/src/graph.rs#L60), [`core/dr-pipeline/src/operation.rs:365`](../../core/dr-pipeline/src/operation.rs#L365), [`ui/dr-ui/ui/app.slint:2501`](../../ui/dr-ui/ui/app.slint#L2501) |
-| FR-DEV-3c | [`core/dr-pipeline/ops/exposure.yaml:1`](../../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:393`](../../core/dr-pipeline/src/graph.rs#L393), [`core/dr-pipeline/src/graph.rs:47`](../../core/dr-pipeline/src/graph.rs#L47), [`core/dr-pipeline/src/mask.rs:1699`](../../core/dr-pipeline/src/mask.rs#L1699), [`core/dr-pipeline/src/preset.rs:751`](../../core/dr-pipeline/src/preset.rs#L751), [`ui/dr-ui/src/develop/rows.rs:928`](../../ui/dr-ui/src/develop/rows.rs#L928) |
+| FR-DEV-3c | [`core/dr-pipeline/ops/exposure.yaml:1`](../../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:393`](../../core/dr-pipeline/src/graph.rs#L393), [`core/dr-pipeline/src/graph.rs:47`](../../core/dr-pipeline/src/graph.rs#L47), [`core/dr-pipeline/src/mask.rs:1744`](../../core/dr-pipeline/src/mask.rs#L1744), [`core/dr-pipeline/src/preset.rs:751`](../../core/dr-pipeline/src/preset.rs#L751), [`ui/dr-ui/src/develop/rows.rs:928`](../../ui/dr-ui/src/develop/rows.rs#L928) |
| FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1014`](../../core/dr-gpu/src/adjust.rs#L1014), [`core/dr-gpu/src/adjust.rs:1107`](../../core/dr-gpu/src/adjust.rs#L1107), [`core/dr-gpu/src/adjust.rs:114`](../../core/dr-gpu/src/adjust.rs#L114), [`core/dr-gpu/src/adjust.rs:798`](../../core/dr-gpu/src/adjust.rs#L798), [`core/dr-gpu/src/adjust.rs:84`](../../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/tests/capture_sharpen.rs:434`](../../core/dr-gpu/tests/capture_sharpen.rs#L434), [`core/dr-gpu/tests/detail_stage.rs:242`](../../core/dr-gpu/tests/detail_stage.rs#L242), [`core/dr-gpu/tests/local_contrast.rs:558`](../../core/dr-gpu/tests/local_contrast.rs#L558), [`core/dr-gpu/tests/noise_reduction.rs:556`](../../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:314`](../../core/dr-pipeline/src/framing.rs#L314), [`core/dr-pipeline/src/graph.rs:991`](../../core/dr-pipeline/src/graph.rs#L991), [`core/dr-pipeline/src/operation.rs:32`](../../core/dr-pipeline/src/operation.rs#L32), [`core/dr-pipeline/src/operation.rs:403`](../../core/dr-pipeline/src/operation.rs#L403), [`core/dr-pipeline/src/operation.rs:53`](../../core/dr-pipeline/src/operation.rs#L53), [`core/dr-pipeline/src/operation.rs:71`](../../core/dr-pipeline/src/operation.rs#L71) |
| FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:121`](../../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:771`](../../core/dr-decode/src/lib.rs#L771), [`core/dr-decode/src/lib.rs:811`](../../core/dr-decode/src/lib.rs#L811), [`core/dr-decode/src/profile.rs:102`](../../core/dr-decode/src/profile.rs#L102), [`core/dr-decode/src/profile.rs:151`](../../core/dr-decode/src/profile.rs#L151), [`core/dr-decode/src/profile.rs:1`](../../core/dr-decode/src/profile.rs#L1), [`core/dr-decode/src/profile.rs:235`](../../core/dr-decode/src/profile.rs#L235), [`core/dr-decode/src/profile.rs:286`](../../core/dr-decode/src/profile.rs#L286), [`core/dr-decode/src/profile.rs:343`](../../core/dr-decode/src/profile.rs#L343), [`core/dr-decode/src/profile.rs:473`](../../core/dr-decode/src/profile.rs#L473), [`core/dr-decode/src/profile.rs:507`](../../core/dr-decode/src/profile.rs#L507), [`core/dr-decode/src/profile.rs:678`](../../core/dr-decode/src/profile.rs#L678), [`core/dr-gpu/src/adjust.rs:37`](../../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:995`](../../core/dr-gpu/src/adjust.rs#L995), [`core/dr-gpu/src/demosaic.rs:121`](../../core/dr-gpu/src/demosaic.rs#L121), [`core/dr-gpu/src/demosaic.rs:86`](../../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs:1985`](../../core/dr-pipeline/src/operation.rs#L1985), [`core/dr-pipeline/src/operation.rs:2066`](../../core/dr-pipeline/src/operation.rs#L2066), [`core/dr-pipeline/src/operation.rs:2091`](../../core/dr-pipeline/src/operation.rs#L2091), [`core/dr-pipeline/src/operation.rs:2106`](../../core/dr-pipeline/src/operation.rs#L2106), [`core/dr-pipeline/src/operation.rs:2130`](../../core/dr-pipeline/src/operation.rs#L2130), [`core/dr-pipeline/src/operation.rs:310`](../../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/operation.rs:471`](../../core/dr-pipeline/src/operation.rs#L471), [`core/dr-pipeline/src/operation.rs:481`](../../core/dr-pipeline/src/operation.rs#L481), [`core/dr-pipeline/src/operation.rs:769`](../../core/dr-pipeline/src/operation.rs#L769) |
| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/boolean_grain.rs:1`](../../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../../core/dr-film/src/lib.rs#L160), [`core/dr-film/src/lib.rs:1`](../../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:149`](../../core/dr-gpu/src/adjust.rs#L149), [`core/dr-gpu/src/adjust.rs:206`](../../core/dr-gpu/src/adjust.rs#L206), [`core/dr-gpu/src/adjust.rs:381`](../../core/dr-gpu/src/adjust.rs#L381), [`core/dr-gpu/src/adjust.rs:510`](../../core/dr-gpu/src/adjust.rs#L510), [`core/dr-gpu/src/adjust.rs:77`](../../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:195`](../../core/dr-gpu/tests/film_sim.rs#L195), [`core/dr-gpu/tests/film_sim.rs:1`](../../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:103`](../../core/dr-pipeline/src/graph.rs#L103), [`core/dr-pipeline/src/graph.rs:168`](../../core/dr-pipeline/src/graph.rs#L168), [`core/dr-pipeline/src/graph.rs:566`](../../core/dr-pipeline/src/graph.rs#L566), [`core/dr-pipeline/src/operation.rs:1313`](../../core/dr-pipeline/src/operation.rs#L1313), [`core/dr-pipeline/src/operation.rs:1341`](../../core/dr-pipeline/src/operation.rs#L1341), [`core/dr-pipeline/src/operation.rs:1387`](../../core/dr-pipeline/src/operation.rs#L1387), [`core/dr-pipeline/src/operation.rs:1985`](../../core/dr-pipeline/src/operation.rs#L1985), [`core/dr-pipeline/src/operation.rs:296`](../../core/dr-pipeline/src/operation.rs#L296), [`core/dr-pipeline/src/operation.rs:310`](../../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/ops/film_sim.rs:102`](../../core/dr-pipeline/src/ops/film_sim.rs#L102), [`core/dr-pipeline/src/ops/film_sim.rs:139`](../../core/dr-pipeline/src/ops/film_sim.rs#L139), [`core/dr-pipeline/src/ops/film_sim.rs:163`](../../core/dr-pipeline/src/ops/film_sim.rs#L163), [`core/dr-pipeline/src/ops/film_sim.rs:1`](../../core/dr-pipeline/src/ops/film_sim.rs#L1), [`core/dr-pipeline/src/ops/film_sim.rs:341`](../../core/dr-pipeline/src/ops/film_sim.rs#L341), [`core/dr-pipeline/src/ops/film_sim.rs:43`](../../core/dr-pipeline/src/ops/film_sim.rs#L43), [`core/dr-pipeline/src/ops/film_sim.rs:97`](../../core/dr-pipeline/src/ops/film_sim.rs#L97), [`core/dr-pipeline/src/sidecar.rs:1055`](../../core/dr-pipeline/src/sidecar.rs#L1055), [`core/dr-pipeline/src/sidecar.rs:130`](../../core/dr-pipeline/src/sidecar.rs#L130), [`core/dr-pipeline/src/sidecar.rs:203`](../../core/dr-pipeline/src/sidecar.rs#L203), [`core/dr-pipeline/src/sidecar.rs:2681`](../../core/dr-pipeline/src/sidecar.rs#L2681), [`core/dr-pipeline/src/sidecar.rs:2757`](../../core/dr-pipeline/src/sidecar.rs#L2757), [`core/dr-pipeline/src/sidecar.rs:576`](../../core/dr-pipeline/src/sidecar.rs#L576), [`core/dr-pipeline/src/sidecar.rs:876`](../../core/dr-pipeline/src/sidecar.rs#L876), [`core/dr-pipeline/src/state.rs:100`](../../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop/render.rs:1252`](../../ui/dr-ui/src/develop/render.rs#L1252), [`ui/dr-ui/src/develop/render.rs:724`](../../ui/dr-ui/src/develop/render.rs#L724), [`ui/dr-ui/src/develop/render.rs:741`](../../ui/dr-ui/src/develop/render.rs#L741), [`ui/dr-ui/src/develop/render.rs:753`](../../ui/dr-ui/src/develop/render.rs#L753), [`ui/dr-ui/src/develop/render.rs:791`](../../ui/dr-ui/src/develop/render.rs#L791), [`ui/dr-ui/src/develop/render.rs:800`](../../ui/dr-ui/src/develop/render.rs#L800), [`ui/dr-ui/src/develop/render.rs:903`](../../ui/dr-ui/src/develop/render.rs#L903), [`ui/dr-ui/src/develop/render.rs:929`](../../ui/dr-ui/src/develop/render.rs#L929), [`ui/dr-ui/src/develop/session.rs:598`](../../ui/dr-ui/src/develop/session.rs#L598), [`ui/dr-ui/src/develop/tabs.rs:80`](../../ui/dr-ui/src/develop/tabs.rs#L80), [`ui/dr-ui/src/develop_ui.rs:386`](../../ui/dr-ui/src/develop_ui.rs#L386), [`ui/dr-ui/src/lib.rs:807`](../../ui/dr-ui/src/lib.rs#L807), [`ui/dr-ui/src/lib.rs:872`](../../ui/dr-ui/src/lib.rs#L872), [`ui/dr-ui/src/library/sidecar.rs:317`](../../ui/dr-ui/src/library/sidecar.rs#L317), [`ui/dr-ui/src/library/sidecar.rs:49`](../../ui/dr-ui/src/library/sidecar.rs#L49), [`ui/dr-ui/src/presets.rs:422`](../../ui/dr-ui/src/presets.rs#L422), [`ui/dr-ui/ui/adjust.slint:1210`](../../ui/dr-ui/ui/adjust.slint#L1210), [`ui/dr-ui/ui/adjust.slint:982`](../../ui/dr-ui/ui/adjust.slint#L982), [`ui/dr-ui/ui/adjust.slint:992`](../../ui/dr-ui/ui/adjust.slint#L992) |
diff --git a/docs/gestures.md b/docs/gestures.md
index 6a84dcd..a286e41 100644
--- a/docs/gestures.md
+++ b/docs/gestures.md
@@ -5,7 +5,7 @@
Every entry here is extracted from the comment beside the code that implements it, so this file cannot describe a gesture the application does not have. Add one by writing a `GESTURE:` block next to the implementation; there is nowhere else to write it.
-47 gestures, in 4 places.
+48 gestures, in 4 places.
## Develop
@@ -189,7 +189,7 @@ The history is forgotten with the sitting, on purpose; a snapshot is the photogr
Disabling a layer is the before-and-after a local edit constantly wants, so it is one press away rather than inside the row. It is an edit and does take a
-`ui/dr-ui/ui/masks.slint:212`
+`ui/dr-ui/ui/masks.slint:213`
### Show or hide one mask on the photograph
@@ -198,7 +198,16 @@ Disabling a layer is the before-and-after a local edit constantly wants, so it i
A mask is judged by seeing where it falls, and two are judged by seeing where they meet — so each row has its own eye rather than the panel having one, and the eye is drawn in the colour the mask shows in, so the row says which shape on the picture is its. Nothing about the edit changes: this is how the photograph is looked at, and takes no history step.
-`ui/dr-ui/ui/masks.slint:279`
+`ui/dr-ui/ui/masks.slint:280`
+
+### Change how a part joins its mask
+
+- **Touch** — Tap the + / − / ∩ chip on the part's row
+- **Pointer** — Click the + / − / ∩ chip on the part's row
+
+A chip that cycles rather than a menu, because a photographer flips a join while looking at the picture, not at the panel: add, take away, keep only where both agree, and round.
+
+`ui/dr-ui/ui/masks.slint:366`
### Leave one part out of a mask, and put it back
@@ -207,7 +216,7 @@ A mask is judged by seeing where it falls, and two are judged by seeing where th
The question a correction raises is whether it did what it was for — whether the stroke filled the shoulder, whether the subtracted gradient took only the sky. Removing it answers that and loses it. The same ring the layer wears, one row down, because it is the same question about a smaller thing.
-`ui/dr-ui/ui/masks.slint:394`
+`ui/dr-ui/ui/masks.slint:405`
## Collections sidebar
diff --git a/ui/dr-ui/src/gesture_book.rs b/ui/dr-ui/src/gesture_book.rs
index 9d92927..78a0093 100644
--- a/ui/dr-ui/src/gesture_book.rs
+++ b/ui/dr-ui/src/gesture_book.rs
@@ -162,6 +162,13 @@ pub const GESTURES: &[Gesture] = &[
pointer: "Click the eye on its row",
keys: "",
},
+ Gesture {
+ title: "Change how a part joins its mask",
+ section: "Develop",
+ touch: "Tap the + / − / ∩ chip on the part's row",
+ pointer: "Click the + / − / ∩ chip on the part's row",
+ keys: "",
+ },
Gesture {
title: "Leave one part out of a mask, and put it back",
section: "Develop",