diff --git a/core/dr-gpu/examples/local.rs b/core/dr-gpu/examples/local.rs index d47fba3..98a0592 100644 --- a/core/dr-gpu/examples/local.rs +++ b/core/dr-gpu/examples/local.rs @@ -326,7 +326,7 @@ fn render_stack( // after the framing map — which is what makes one mask correct at every // output size, zoom and crop. let array = masks - .render(stack, None, Some(&subjects), pw, ph) + .render(stack, None, Some(&subjects), Some(source), pw, ph) .expect("rasterise masks"); let shader = compose_full( diff --git a/core/dr-gpu/src/mask.rs b/core/dr-gpu/src/mask.rs index faadecb..09884c6 100644 --- a/core/dr-gpu/src/mask.rs +++ b/core/dr-gpu/src/mask.rs @@ -21,6 +21,17 @@ //! boxes, one draw each, compositing onto the slice with blend state — see the //! second half of `mask.wgsl`. //! +//! # The photograph, bound as an input +//! +//! A range mask (FR-DEV-10) selects by what a pixel *is*, so this pass reads +//! the demosaiced source as well as writing masks. It is bound for every draw +//! and looked at by two modes; everything else gets a 1x1 placeholder, for the +//! reason the label field below does — the bindings are fixed, and a second +//! pipeline differing only in what it ignores costs more than a texel. +//! +//! Nothing is read back and nothing is rasterised on this side. What crosses +//! into CPU memory for a range layer is five floats and a matrix. +//! //! # The label field //! //! Region masks index a compacted label field uploaded once per segmentation. @@ -31,9 +42,10 @@ //! same place and cadence the region adjacency graph is already built at. use dr_pipeline::mask::{MaskSource, MaskStack, Stroke, MAX_LAYERS}; + use wgpu::util::DeviceExt; -use crate::{GpuContext, GpuError}; +use crate::{DemosaicedImage, GpuContext, GpuError}; /// Modes understood by `mask.wgsl`. Kept beside the shader's `switch`. const MODE_REGIONS: u32 = 0; @@ -43,6 +55,10 @@ const MODE_SUBJECT: u32 = 3; /// Brush layers go through their own entry points rather than the `switch`, so /// this is only ever read by a person looking at a captured frame. const MODE_BRUSH: u32 = 4; +/// TRACES: FR-DEV-10 +const MODE_LUMINANCE: u32 = 5; +/// TRACES: FR-DEV-10 +const MODE_COLOUR: u32 = 6; /// Six vertices — two triangles — per stroke. See `vs_brush`. const VERTICES_PER_STROKE: u32 = 6; @@ -66,7 +82,19 @@ struct MaskParams { axis: [f32; 2], softness: f32, angle: f32, - _pad1: [f32; 2], + /// TRACES: FR-DEV-10 + /// Source texels per mask texel, per axis. See `image_value` in the + /// shader for why a range averages its footprint rather than sampling it. + source_step: [f32; 2], + + /// Camera RGB → linear sRGB, one row per `vec4` because that is the + /// alignment a uniform gives a three-component vector anyway. Only a + /// range mask reads them. + cam_to_srgb: [[f32; 4]; 3], + /// `rgb`: as-shot white balance. `w`: non-zero for a gamma-encoded source. + /// The same packing the generated adjust shader uses, so the two agree by + /// construction rather than by inspection. + as_shot_wb: [f32; 4], } /// One stroke, as `mask.wgsl`'s `StrokeHeader` expects it. @@ -371,6 +399,14 @@ pub struct MaskPass { /// label slots even when rasterising a gradient. A placeholder is cheaper /// and far simpler than two pipelines differing only in what they ignore. placeholder: LabelField, + /// TRACES: FR-DEV-10 + /// Bound at the image slot for every mask that is not a range. + /// + /// Never sampled by those modes, so its contents do not matter — but it is + /// cleared rather than left undefined, because a placeholder whose value + /// is arbitrary is one that makes a binding mistake look like a mask that + /// nearly works. + empty_image: wgpu::TextureView, } impl MaskPass { @@ -405,6 +441,20 @@ impl MaskPass { }, count: None, }, + // TRACES: FR-DEV-10 + // The photograph, for a range mask. Unfilterable for the + // same reason the field above is: every read is a + // `textureLoad`, and this pipeline binds no sampler. + wgpu::BindGroupLayoutEntry { + binding: 6, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: false }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, ], }); @@ -515,6 +565,7 @@ impl MaskPass { } let placeholder = LabelField::upload(ctx, &[0], 1, 1, 0)?; + let empty_image = empty_image(ctx); // Everywhere outside, so a layer that somehow reaches this masks // nothing rather than everything. let empty_subject = SubjectMasks::upload(ctx, &[&[-1.0f32][..]], 1, 1)?; @@ -530,6 +581,7 @@ impl MaskPass { allocations: 0, placeholder, empty_subject, + empty_image, }) } @@ -538,11 +590,17 @@ impl MaskPass { /// `labels` may be `None` when no layer is a region mask; a region layer /// without one is skipped rather than drawn wrong, since a mask that /// silently covers the whole frame would apply an edit everywhere. + /// + /// `source` is the photograph a range layer measures (FR-DEV-10), and it + /// is skipped on the same rule for the same reason: without it the shader + /// would read a blank placeholder, and a band that happens to contain + /// black would then cover the whole frame. pub fn render( &mut self, stack: &MaskStack, labels: Option<&LabelField>, subjects: Option<&SubjectMasks>, + source: Option<&DemosaicedImage>, width: u32, height: u32, ) -> Result<&MaskArray, GpuError> { @@ -597,7 +655,24 @@ impl MaskPass { _ => (&self.empty_subject, 0), }; - let params = self.params(layer, field, width, height); + // TRACES: FR-DEV-10 + // A range layer with no photograph bound is skipped rather than + // drawn against the placeholder, on exactly the rule the two + // cases above follow: an absent mask that defaults to "everything" + // takes a local adjustment global, which is a far quieter failure + // than a layer that visibly did not render. + let image = match (&layer.source, source) { + (s, None) if s.is_range() => { + log::warn!( + "mask layer {} selects a range with no image loaded; skipping", + layer.id + ); + continue; + } + (_, image) => image, + }; + + let params = self.params(layer, field, image, width, height); match &layer.source { MaskSource::Brush { strokes } => { self.draw_brush(&mut encoder, slot as u32, ¶ms, strokes, width, height) @@ -611,6 +686,7 @@ impl MaskPass { field, &selected, subject, + image, ); } } @@ -634,9 +710,34 @@ impl MaskPass { &self, layer: &dr_pipeline::mask::MaskLayer, field: &LabelField, + source: Option<&DemosaicedImage>, width: u32, height: u32, ) -> MaskParams { + // TRACES: FR-DEV-10 + // How much of the photograph one mask texel covers. One when there is + // no image bound, which is a value nothing reads — the range modes are + // the only readers and they are skipped in that case. + let source_step = match source { + Some(image) => { + let (sw, sh) = image.size(); + [ + sw as f32 / width.max(1) as f32, + sh as f32 / height.max(1) as f32, + ] + } + None => [1.0, 1.0], + }; + // Row-major nine, widened to three `vec4`s. Identity where there is no + // image, so a range that somehow reached the shader without one would + // read camera values rather than nothing — the same defensive choice + // the demosaicer makes for an uncalibrated body. + let m = source.map_or([1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], |i| { + i.color_matrix() + }); + let wb = source.map_or([1.0, 1.0, 1.0], |i| i.as_shot_wb()); + let non_linear = source.is_some_and(|i| i.is_non_linear()); + let base = MaskParams { width, height, @@ -650,7 +751,13 @@ impl MaskPass { axis: [1.0, 0.0], softness: 0.0, angle: 0.0, - _pad1: [0.0, 0.0], + source_step, + cam_to_srgb: [ + [m[0], m[1], m[2], 0.0], + [m[3], m[4], m[5], 0.0], + [m[6], m[7], m[8], 0.0], + ], + as_shot_wb: [wb[0], wb[1], wb[2], if non_linear { 1.0 } else { 0.0 }], }; match &layer.source { @@ -719,6 +826,33 @@ impl MaskPass { mode: MODE_BRUSH, ..base }, + // TRACES: FR-DEV-10 + // A band, carried in the fields the gradients measure geometry + // in. Reused rather than given their own, and it is not a + // shortcut: `centre` and `axis` are two pairs of floats whose + // meaning has always been the mode's to decide, and a range that + // added four more would grow the uniform every other mask pays + // for. What matters is that nothing here is a *coordinate* — a + // range is not a function of position at all. + MaskSource::Luminance { lo, hi, softness } => MaskParams { + mode: MODE_LUMINANCE, + axis: [*lo, *hi], + softness: *softness, + ..base + }, + MaskSource::Colour { + hue, + hue_width, + chroma_lo, + chroma_hi, + softness, + } => MaskParams { + mode: MODE_COLOUR, + centre: [*hue, *hue_width], + axis: [*chroma_lo, *chroma_hi], + softness: *softness, + ..base + }, } } @@ -887,6 +1021,7 @@ impl MaskPass { field: &LabelField, selected: &wgpu::Buffer, subject: (&SubjectMasks, usize), + source: Option<&DemosaicedImage>, ) { let params_buf = self .ctx @@ -924,6 +1059,13 @@ impl MaskPass { }), ), }, + // TRACES: FR-DEV-10 + wgpu::BindGroupEntry { + binding: 6, + resource: wgpu::BindingResource::TextureView( + source.map_or(&self.empty_image, |i| i.view()), + ), + }, ], }); @@ -1005,6 +1147,38 @@ impl MaskPass { } } +/// TRACES: FR-DEV-10 +/// A single black texel, bound at the image slot for a mask that is not a +/// range. +/// +/// Written rather than merely allocated. Undefined contents would be read by +/// nothing today, but a binding mistake in a range mask would then produce +/// whatever the driver left in memory — a mask that flickers between builds +/// and machines, which is the hardest shape of bug this pass could have. +fn empty_image(ctx: &GpuContext) -> wgpu::TextureView { + let texture = ctx.device.create_texture_with_data( + &ctx.queue, + &wgpu::TextureDescriptor { + label: Some("mask-empty-image"), + size: wgpu::Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: DemosaicedImage::FORMAT, + usage: wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }, + wgpu::util::TextureDataOrder::LayerMajor, + // Four half-floats of zero. Rgba16Float, so eight bytes. + &[0u8; 8], + ); + texture.create_view(&wgpu::TextureViewDescriptor::default()) +} + /// The shorter edge of the space the mask is rasterised in. /// /// Feather and morphology are stored as fractions of it, so the same edit is @@ -1131,7 +1305,7 @@ mod tests { let mut pass = MaskPass::new(&ctx).expect("mask pass"); let array = pass - .render(&stack, Some(&field), None, w, h) + .render(&stack, Some(&field), None, None, w, h) .expect("render"); assert_eq!(array.size(), (w, h)); assert_eq!(array.layers(), 1); @@ -1153,7 +1327,7 @@ mod tests { })); let mut pass = MaskPass::new(&ctx).expect("mask pass"); - assert!(pass.render(&stack, None, None, 8, 8).is_ok()); + assert!(pass.render(&stack, None, None, None, 8, 8).is_ok()); } #[test] @@ -1176,7 +1350,9 @@ mod tests { })); let mut pass = MaskPass::new(&ctx).expect("mask pass"); - let array = pass.render(&stack, None, None, 16, 16).expect("render"); + let array = pass + .render(&stack, None, None, None, 16, 16) + .expect("render"); assert_eq!(array.layers(), 2, "one slice per active layer"); } @@ -1208,7 +1384,9 @@ mod tests { stack.push(painted(&[(false, 0.1, vec![(0.2, 0.2), (0.8, 0.8)])])); let mut pass = MaskPass::new(&ctx).expect("mask pass"); - let array = pass.render(&stack, None, None, 32, 32).expect("render"); + let array = pass + .render(&stack, None, None, None, 32, 32) + .expect("render"); assert_eq!(array.layers(), 1); } @@ -1258,7 +1436,7 @@ mod tests { }; let mut pass = MaskPass::new(&ctx).expect("mask pass"); let array = pass - .render(&MaskStack::new(), None, None, 8, 8) + .render(&MaskStack::new(), None, None, None, 8, 8) .expect("render"); assert_eq!( array.layers(), @@ -1281,17 +1459,20 @@ mod tests { })); let mut pass = MaskPass::new(&ctx).expect("mask pass"); - pass.render(&stack, None, None, 32, 32).expect("render"); + pass.render(&stack, None, None, None, 32, 32) + .expect("render"); assert_eq!(pass.allocations(), 1); - pass.render(&stack, None, None, 32, 32).expect("render"); + pass.render(&stack, None, None, None, 32, 32) + .expect("render"); assert_eq!( pass.allocations(), 1, "same size and layer count should not reallocate" ); - pass.render(&stack, None, None, 64, 64).expect("render"); + pass.render(&stack, None, None, None, 64, 64) + .expect("render"); assert_eq!(pass.allocations(), 2, "a resize must reallocate"); } } diff --git a/core/dr-gpu/src/shaders/mask.wgsl b/core/dr-gpu/src/shaders/mask.wgsl index 89abf4a..3cfc202 100644 --- a/core/dr-gpu/src/shaders/mask.wgsl +++ b/core/dr-gpu/src/shaders/mask.wgsl @@ -28,7 +28,8 @@ struct MaskParams { label_width: u32, label_height: u32, - // 0 = regions, 1 = linear, 2 = radial, 3 = subject, 4 = brush. + // 0 = regions, 1 = linear, 2 = radial, 3 = subject, 4 = brush, + // 5 = luminance range, 6 = colour range. // // A brush does not read this — it has its own entry points, because it is // the one mask that is not a function of the whole frame — but it is set @@ -50,10 +51,33 @@ struct MaskParams { // Linear: (cos, sin) of the ramp direction. Radial: semi-axes. axis: vec2, // Linear: ramp width. Radial: edge falloff as a fraction of the radius. + // A range: the fade at each edge of its band, in the band's own units. softness: f32, // Radial only: rotation of the ellipse. angle: f32, - _pad1: vec2, + + // TRACES: FR-DEV-10 + // How many source texels one mask texel spans, per axis. + // + // The mask array is rasterised at a proxy size and the photograph is not, + // so one texel here covers several there. A range mask is a function of + // pixel *values*, and point-sampling one source texel in four would make + // its edge follow the sensor's noise wherever the picture has fine + // texture — speckle that is then a mask, and therefore visible in the + // adjustment. Averaging the footprint is what makes the band land on the + // tone the area actually is. + source_step: vec2, + + // Camera RGB → linear sRGB, one row each. Only a range reads these: it is + // the one mask that looks at the photograph, and a hue is the body's own + // primaries until this matrix has been applied — so the same stored arc + // would select a different set of colours on every make of sensor. + cam_to_srgb_0: vec4, + cam_to_srgb_1: vec4, + cam_to_srgb_2: vec4, + // rgb: as-shot white balance. w: non-zero when the source arrived + // gamma-encoded rather than linear. + as_shot_wb: vec4, } @group(0) @binding(0) var p: MaskParams; @@ -74,6 +98,19 @@ struct MaskParams { // shrinking and feathering free: each is arithmetic on this, so a slider moves // a uniform instead of rebuilding a mask. @group(0) @binding(3) var subject: texture_2d; +// TRACES: FR-DEV-10 +// The photograph itself, as the demosaicer left it: camera RGB, unbalanced, +// with no edit applied. A 1x1 placeholder for every mask that is a shape, +// because the bindings are fixed and a second pipeline differing only in what +// it ignores would cost more than one texel. +// +// **The unedited image, and that is the design rather than an accident of +// pass order.** A band over the *edited* result would move as the edit was +// made: raising the highlights would change which pixels counted as +// highlights, so the slider would chase its own mask. Measuring what the +// camera recorded means the selection stays where the photographer put it +// while they work on it. +@group(0) @binding(6) var image: texture_2d; // A full-screen triangle rather than a quad: three vertices instead of six, // no shared edge for the rasteriser to crack along, and no vertex buffer. @@ -221,6 +258,177 @@ fn subject_mask(uv: vec2) -> f32 { } } +// --------------------------------------------------------------------------- +// Range masks (FR-DEV-10) +// --------------------------------------------------------------------------- +// +// The masks that select by what a pixel *is* rather than by where it sits. +// Nothing below reads `frame_delta`, and that absence is the point: a range is +// not a function of position, so it cannot be stretched by an aspect ratio, +// cannot drift under a crop, and comes out the same at a proxy size and at an +// export because the only thing it depends on is the photograph's own values. +// +// The band arrives entirely in the fields the gradients use — `axis` is the +// pair of bounds, `centre` is a colour range's arc, `softness` is the fade — +// so a range costs nothing in the uniform beyond the image transform above. + +// Display-encoded sRGB back to linear. +// +// A JPEG is uploaded with its bytes untouched, so its values are gamma-encoded +// where the demosaicer's are linear. The same undoing the generated adjust +// shader does, at the same point and for the same reason: a band over +// brightness is meaningless if two sources disagree about what a value means. +fn decode_srgb(c: vec3) -> vec3 { + let lo = c / 12.92; + let hi = pow((max(c, vec3(0.04045)) + 0.055) / 1.055, vec3(2.4)); + return select(hi, lo, c <= vec3(0.04045)); +} + +// One source texel, as linear sRGB. +// +// This is the prologue of the generated adjust shader, repeated: decode, +// balance, pull a clipped pixel back to neutral, then the camera matrix. It is +// repeated rather than shared because the composer emits WGSL for the *edit* +// and this pass is not one — but it must agree with it, since a range mask +// exists to select the values the layer's own adjustments will then see. +// +// The highlight desaturation is the part that looks skippable and is not. A +// fully clipped photosite arrives as (1,1,1), carrying no colour at all; the +// as-shot multipliers are far from neutral, so balancing it and passing it +// through the matrix produces a strong magenta. A colour range would then +// select every blown sky as if the photographer had asked for magenta. +fn source_texel(px: vec2) -> vec3 { + var c = textureLoad(image, px, 0).rgb; + if (p.as_shot_wb.w > 0.5) { + c = decode_srgb(c); + } + + let clipped = smoothstep(0.985, 1.0, max(c.r, max(c.g, c.b))); + c = c * p.as_shot_wb.rgb; + if (clipped > 0.0) { + c = mix(c, vec3(max(c.r, max(c.g, c.b))), clipped); + } + + return vec3( + dot(p.cam_to_srgb_0.rgb, c), + dot(p.cam_to_srgb_1.rgb, c), + dot(p.cam_to_srgb_2.rgb, c), + ); +} + +// The most taps one mask texel averages, per axis. +// +// A cap rather than the true footprint. At a 1600 px proxy over a 24 MP frame +// the ratio is under four, so this is the whole footprint for every ordinary +// photograph; past it the taps stride across the footprint instead of +// covering it, which is a sample of the area rather than its mean. That is the +// right way to run out of budget here — the estimate gets noisier, it does not +// start measuring somewhere else. +const MAX_SOURCE_TAPS: i32 = 4; + +// The photograph's value under one mask texel, in linear sRGB. +fn image_value(px: vec2) -> vec3 { + let dims = vec2(textureDimensions(image)); + let last = dims - vec2(1); + + // The footprint's top-left corner in source texels. Not a centre plus a + // radius: the mask texel is a *box* over the source, and sampling + // symmetrically about its centre would weight the middle of every + // footprint twice at odd tap counts. + let origin = vec2(px) * p.source_step; + let taps = clamp(vec2(ceil(p.source_step)), vec2(1), vec2(MAX_SOURCE_TAPS)); + // `stride`, not `step`: WGSL has a builtin of that name, and a local that + // shadows one is legal and unreadable in the same breath. + let stride = p.source_step / vec2(taps); + + var total = vec3(0.0); + for (var y = 0; y < taps.y; y = y + 1) { + for (var x = 0; x < taps.x; x = x + 1) { + let at = origin + (vec2(f32(x), f32(y)) + vec2(0.5)) * stride; + total = total + source_texel(clamp(vec2(at), vec2(0), last)); + } + } + return total / f32(taps.x * taps.y); +} + +// A soft band: one inside, nothing outside, a smooth ramp across each edge. +// +// The `min` rather than a product of the two ramps. A band narrower than twice +// its softness has no plateau, and multiplying the rising and falling ramps +// would then peak well below one — so "select the highlights" would come out +// at sixty per cent and the photographer would compensate with opacity, +// against a mask that was quietly weaker than it said. `min` keeps the +// plateau where there is one and degrades to a single peak where there is not. +fn band(v: f32, lo: f32, hi: f32, soft: f32) -> f32 { + if (soft <= 0.0) { + return select(0.0, 1.0, v >= lo && v <= hi); + } + return min(smoothstep(lo - soft, lo, v), 1.0 - smoothstep(hi, hi + soft, v)); +} + +fn luminance_mask(px: vec2) -> f32 { + let y = dot(image_value(px), vec3(0.2126, 0.7152, 0.0722)); + // Onto the perceptual position `tone_position` in `ops/_helpers.yaml` + // establishes, which is where the stored bounds are measured. Linear light + // puts middle grey at 0.18, so a band stated in it would spend four fifths + // of its travel inside the shadows. + let t = clamp(pow(max(y, 0.0), 1.0 / 3.0), 0.0, 1.0); + return band(t, p.axis.x, p.axis.y, p.softness); +} + +// Hue in turns, 0 at red and increasing through yellow. +// +// The plain six-sector definition. Zero for a neutral, which is a value the +// caller must not act on — the chroma bound below is what keeps a colour range +// away from the greys where this number is rounding noise. +fn hue_of(c: vec3) -> f32 { + let hi = max(c.r, max(c.g, c.b)); + let lo = min(c.r, min(c.g, c.b)); + let d = hi - lo; + if (d <= 0.0) { + return 0.0; + } + var h = 0.0; + if (hi == c.r) { + h = (c.g - c.b) / d; + } else if (hi == c.g) { + h = (c.b - c.r) / d + 2.0; + } else { + h = (c.r - c.g) / d + 4.0; + } + return fract(h / 6.0); +} + +fn colour_mask(px: vec2) -> f32 { + let c = max(image_value(px), vec3(0.0)); + let hi = max(c.r, max(c.g, c.b)); + let lo = min(c.r, min(c.g, c.b)); + // The max-minus-min chroma `colour_saturation` uses, so the number the + // band is stated in is the one the rest of the pipeline means by + // 'colourfulness'. + var chroma = 0.0; + if (hi > 0.0) { + chroma = (hi - lo) / hi; + } + + // Distance round the circle, so an arc centred near red reaches both ways + // past zero. Written as a wrap rather than as two comparisons because red + // is exactly where skin sits, and an arc that stopped at the seam would + // select half of it. + let d = abs(fract(hue_of(c) - p.centre.x + 0.5) - 0.5); + var arc = 0.0; + if (p.softness <= 0.0) { + arc = select(0.0, 1.0, d <= p.centre.y); + } else { + arc = 1.0 - smoothstep(p.centre.y, p.centre.y + p.softness, d); + } + + // Both, not either: an arc alone selects a haze of noise everywhere the + // picture is nearly grey, because a hue rounded out of three almost-equal + // channels is still a hue. + return min(arc, band(chroma, p.axis.x, p.axis.y, p.softness)); +} + @fragment fn fs(@builtin(position) pos: vec4) -> @location(0) vec4 { let px = vec2(i32(pos.x), i32(pos.y)); @@ -235,6 +443,8 @@ fn fs(@builtin(position) pos: vec4) -> @location(0) vec4 { case 1u: { m = linear_mask(uv); } case 2u: { m = radial_mask(uv); } case 3u: { m = subject_mask(uv); } + case 5u: { m = luminance_mask(px); } + case 6u: { m = colour_mask(px); } default: { m = 0.0; } } diff --git a/core/dr-gpu/tests/local_adjustments.rs b/core/dr-gpu/tests/local_adjustments.rs index c7dfb08..ec9fa39 100644 --- a/core/dr-gpu/tests/local_adjustments.rs +++ b/core/dr-gpu/tests/local_adjustments.rs @@ -76,7 +76,9 @@ fn render_at( ); let mut masks = MaskPass::new(ctx).expect("mask pass"); - let array = masks.render(stack, field, None, w, h).expect("rasterise"); + let array = masks + .render(stack, field, None, None, w, h) + .expect("rasterise"); let mut adjust = AdjustPass::new(ctx); adjust diff --git a/core/dr-gpu/tests/masked_outputs.rs b/core/dr-gpu/tests/masked_outputs.rs index 7e2b2aa..41093bb 100644 --- a/core/dr-gpu/tests/masked_outputs.rs +++ b/core/dr-gpu/tests/masked_outputs.rs @@ -81,7 +81,7 @@ fn render_at(ctx: &GpuContext, stack: &MaskStack, out: u32) -> Vec { let mut masks = MaskPass::new(ctx).expect("mask pass"); let array = masks - .render(stack, None, Some(&subjects), PROXY, PROXY) + .render(stack, None, Some(&subjects), None, PROXY, PROXY) .expect("rasterise"); let shader = compose_full( @@ -224,7 +224,9 @@ fn render_gradient(ctx: &GpuContext, stack: &MaskStack, w: u32, h: u32) -> Vec Option { + pollster::block_on(GpuContext::new_headless()).ok() +} + +/// A frame whose left half is one colour and right half another. +/// +/// Deliberately not a ramp. A range mask's whole job is to divide the picture +/// by value, so a source that is already divided by value makes the assertion +/// "the mask found the half it was aimed at" rather than "the mask is roughly +/// where it should be". +fn split(ctx: &GpuContext, left: [u8; 3], right: [u8; 3]) -> DemosaicedImage { + let data: Vec = (0..SIZE * SIZE) + .flat_map(|i| { + let c = if i % SIZE < SIZE / 2 { left } else { right }; + [c[0], c[1], c[2], 255] + }) + .collect(); + DemosaicedImage::from_rgba8(ctx, &data, SIZE, SIZE).expect("upload") +} + +fn brightened(source: MaskSource) -> MaskStack { + let mut stack = MaskStack::new(); + let mut layer = MaskLayer::new("m1", source); + // Two stops, so "selected" and "not selected" are not a judgement call. + layer.set_param("exposure", ParamId("exposure"), 2.0); + stack.push(layer); + stack +} + +/// Render `stack` over `source`, with the mask rasterised at `raster`. +/// +/// `raster` is a parameter because it is the thing that must not matter: the +/// develop view and an export ask for the same mask at different sizes, and a +/// range that answered differently at each would be a mask that changes when +/// the photograph is exported. +fn render(ctx: &GpuContext, source: &DemosaicedImage, stack: &MaskStack, raster: u32) -> Vec { + let mut masks = MaskPass::new(ctx).expect("mask pass"); + let array = masks + .render(stack, None, None, Some(source), raster, raster) + .expect("rasterise"); + + let shader = compose_full( + &ops::chain(), + &Framing::new(), + ColourSpace::Srgb, + stack, + &SpotSet::new(), + &[], + ); + let mut adjust = AdjustPass::new(ctx); + adjust + .render_masked(source, &shader, SIZE, SIZE, Some(array)) + .expect("render"); + adjust.export_pixels().expect("readback").0 +} + +fn red_at(pixels: &[u8], x: u32, y: u32) -> u8 { + pixels[((y * SIZE + x) * 4) as usize] +} + +/// The centre of each half, away from the seam the mask's own softness +/// straddles. +fn halves(pixels: &[u8]) -> (u8, u8) { + ( + red_at(pixels, SIZE / 4, SIZE / 2), + red_at(pixels, SIZE * 3 / 4, SIZE / 2), + ) +} + +/// TRACES: FR-DEV-10 +#[test] +fn a_tone_band_brightens_only_the_half_inside_it() { + let Some(ctx) = ctx() else { + eprintln!("no adapter; skipping"); + return; + }; + // 200 lands near 0.83 on the perceptual scale and 30 near 0.24, so a band + // over the upper half contains one and not the other with room to spare. + let source = split(&ctx, [200, 200, 200], [30, 30, 30]); + let stack = brightened(MaskSource::luminance_range(0.5, 1.0, 0.15)); + + let pixels = render(&ctx, &source, &stack, SIZE); + let (bright, dark) = halves(&pixels); + + assert!( + bright > 230, + "the bright half is inside the band and should have been lifted, got {bright}" + ); + assert!( + dark < 45, + "the dark half is outside the band and must be untouched, got {dark}" + ); +} + +/// TRACES: FR-DEV-10 +/// The property the develop view and the export path share. The mask array is +/// rasterised at a proxy size in both, but nothing in this pass may *depend* +/// on that size — a range is a function of the photograph's values, and those +/// do not change when somebody asks for a bigger picture. +#[test] +fn a_tone_band_is_the_same_mask_at_any_raster_size() { + let Some(ctx) = ctx() else { + eprintln!("no adapter; skipping"); + return; + }; + let source = split(&ctx, [200, 200, 200], [30, 30, 30]); + let stack = brightened(MaskSource::luminance_range(0.5, 1.0, 0.15)); + + // A quarter of the source and twice it: one mask texel averaging sixteen + // source texels, and one source texel spread over four mask texels. + let small = halves(&render(&ctx, &source, &stack, SIZE / 4)); + let large = halves(&render(&ctx, &source, &stack, SIZE * 2)); + + // A tolerance rather than equality: the two rasters land their edges on + // different grids, and the assertion is that the *selection* is the same, + // not that two resamplings of it are bit-identical. + assert!( + small.0.abs_diff(large.0) <= 4 && small.1.abs_diff(large.1) <= 4, + "the same band selected differently at two raster sizes: {small:?} vs {large:?}" + ); +} + +/// TRACES: FR-DEV-10 +#[test] +fn a_colour_band_follows_hue_rather_than_brightness() { + let Some(ctx) = ctx() else { + eprintln!("no adapter; skipping"); + return; + }; + // Short of saturation on purpose. A fully clipped channel carries no + // colour at all, and the pass pulls such a pixel back to neutral before + // the band ever sees it — which is correct, and would make this test about + // that instead. + let source = split(&ctx, [200, 40, 40], [40, 40, 200]); + // A narrow arc at red, above the chroma floor that keeps the greys out. + let stack = brightened(MaskSource::colour_range(0.0, 0.05, 0.15, 1.0, 0.05)); + + let pixels = render(&ctx, &source, &stack, SIZE); + let (red, blue) = halves(&pixels); + + assert!( + red > 230, + "the red half is inside the arc and should have been lifted, got {red}" + ); + assert!( + blue < 60, + "the blue half is a third of the circle away and must be untouched, got {blue}" + ); +} + +/// TRACES: FR-DEV-10 +/// The greys a hue arc would otherwise sweep up. +/// +/// A nearly-neutral pixel still has a hue — three almost-equal channels round +/// to one — so an arc without a chroma floor selects a haze of noise across +/// every desaturated part of the picture. It is the failure that looks like the +/// mask working badly rather than like a control that is missing. +#[test] +fn a_colour_band_ignores_the_greys() { + let Some(ctx) = ctx() else { + eprintln!("no adapter; skipping"); + return; + }; + // Both halves neutral, at the two brightnesses the tone test uses, so the + // only reason either could be selected is the hue arc reaching them. + let source = split(&ctx, [200, 200, 200], [30, 30, 30]); + let stack = brightened(MaskSource::colour_range(0.0, 0.5, 0.15, 1.0, 0.05)); + + let pixels = render(&ctx, &source, &stack, SIZE); + let (light, dark) = halves(&pixels); + + assert!( + light < 215 && dark < 45, + "a grey frame was selected by a colour mask: {light}, {dark}" + ); +} diff --git a/core/dr-pipeline/src/mask.rs b/core/dr-pipeline/src/mask.rs index 1c529b5..454bbd7 100644 --- a/core/dr-pipeline/src/mask.rs +++ b/core/dr-pipeline/src/mask.rs @@ -50,6 +50,24 @@ //! still merges per field. The raster sits beside it as a cache, takes no part //! in equality, and is thrown away rather than trusted when it does not fit. //! See [`crate::coverage`]. +//! +//! # The masks that are not shapes +//! +//! Everything above selects by *where*: a region, an instance, a gradient's +//! geometry, the path a finger took. [`MaskSource::Luminance`] and +//! [`MaskSource::Colour`] select by *what a pixel is* — a band over the +//! photograph's own brightness or its own colour, with the edges of the band +//! soft (FR-DEV-10). That is what a luminosity mask has always been, and it is +//! why a local edit made through one blends without a halo: the boundary +//! follows the picture's values exactly, at whatever detail the picture has, +//! rather than an outline drawn near them. +//! +//! They store the band and nothing else, for precisely the reason the region +//! ids above are ids: five numbers diff, sync and merge per field under +//! FR-NC-9, where the pixels they select would be a blob for a merge to +//! arbitrate. And the pixels never need storing, because unlike a model's +//! coverage they are reproducible by anything that can read the photograph — +//! which is every path that renders one. use std::fmt::Write as _; use std::sync::Arc; @@ -104,6 +122,27 @@ pub const MAX_STROKE_POINTS: usize = 256; /// work the user can see on screen must not vanish without being told. pub const MAX_LAYER_POINTS: usize = 4096; +/// TRACES: FR-DEV-10 +/// How far a range mask fades in and out at each edge of its band. +/// +/// In the band's own units — tone position for a luminance range, chroma or +/// turns of hue for a colour one — because that is the only place a range +/// mask has an edge. It has no boundary in the picture and therefore no +/// distance from one, which is why [`MaskLayer::feather`] cannot reach it. +/// +/// The default is generous on purpose. A hard band over a photograph's values +/// finds the contour lines of the picture and draws them, and a tone contour +/// crossing a smooth sky is the one artefact a luminosity mask exists to +/// avoid. Softness is what turns the band from a threshold into a weighting. +pub const DEFAULT_RANGE_SOFTNESS: f32 = 0.15; + +/// The widest a hue arc may be, in turns. +/// +/// Half a turn each way is the whole circle, so anything past this is not a +/// selection — it is every colour, arrived at by a control the user thought +/// was narrowing something. +pub const MAX_HUE_WIDTH: f32 = 0.5; + /// Brush radius a new stroke starts at, as a fraction of the shorter edge. pub const DEFAULT_BRUSH_RADIUS: f32 = 0.05; /// Fraction of the radius that is fully covered before the edge falls away. @@ -148,6 +187,21 @@ fn snap(v: f32) -> f32 { (v * STROKE_GRID).round() / STROKE_GRID } +/// Two band edges, clamped to `0..=1` and put the right way round. +/// +/// Swapped rather than collapsed to a point: dragging one handle past the +/// other is how every range control in every editor is used to reverse a +/// selection's ends, and the alternative — clamping the lower bound against +/// the upper — makes the band stick at zero width and the drag stop dead. +fn ordered(lo: f32, hi: f32) -> (f32, f32) { + let (lo, hi) = (lo.clamp(0.0, 1.0), hi.clamp(0.0, 1.0)); + if lo <= hi { + (lo, hi) + } else { + (hi, lo) + } +} + /// TRACES: FR-DEV-3 /// One painted stroke: a disc of radius `radius` swept along a polyline. /// @@ -651,6 +705,82 @@ pub enum MaskSource { /// thing it was painted on through a crop, a straighten and an export at /// any size. Brush { strokes: Vec }, + + /// TRACES: FR-DEV-10 + /// Every pixel whose brightness falls in a band — the luminosity mask. + /// + /// The first mask in this enum that is not a shape. A gradient, a stroke + /// and a region all answer "where"; this answers "how bright", and the + /// selection that comes out has the edge the *photograph* has rather than + /// an edge somebody drew near it. That is the whole reason a local edit + /// made through one blends: there is no boundary to halo along. + /// + /// # Why the band is in perceptual position, not in linear light + /// + /// The bounds are positions on the same 0..1 lightness scale + /// `ops/_helpers.yaml`'s `tone_position` establishes, which is the cube + /// root of luminance. Linear light puts middle grey at 0.18, so a band + /// stated in it would call four fifths of the range "shadow" and the + /// slider's useful travel would be its first inch. The stored numbers are + /// where the eye puts them, which is also where the control puts them. + /// + /// # Which image it measures + /// + /// The photograph as captured, before this edit — not the edited result. + /// The mask is rasterised in its own pass, ahead of the adjustment that + /// samples it, so there is no arrangement in which it could read its own + /// output. That is a property worth having rather than a limitation + /// worked around: a band over the edited image would slide out from under + /// the edit as the edit was made, so raising the highlights would change + /// which pixels counted as highlights, and the slider would fight itself. + Luminance { + /// Lower edge of the band, as a tone position in `0.0..=1.0`. + lo: f32, + /// Upper edge, in the same units. Never below `lo` — see + /// [`MaskSource::luminance_range`], which is where that is kept true. + hi: f32, + /// How far the band fades in below `lo` and out above `hi`, in the + /// same units. Zero is a threshold, and a threshold over a + /// photograph's own tones draws contour lines. + softness: f32, + }, + + /// TRACES: FR-DEV-10 + /// Every pixel whose colour falls in a band — the skin-tone mask. + /// + /// The counterpart to [`Self::Luminance`], and the pair is not symmetric: + /// brightness is one number and colour is two, so this carries an arc of + /// hue *and* a range of chroma. Both are needed, and the chroma half is + /// the one that is easy to leave out. Hue is meaningless as a colour + /// approaches grey — a nearly-neutral wall has a hue, arrived at by + /// rounding — so an arc alone selects a haze of noise everywhere the + /// picture is desaturated. The lower chroma bound is what keeps that out, + /// and it is why the default sits well above zero. + /// + /// Measured in linear sRGB, after the camera matrix, because that is the + /// only space in which a stated hue means the colour the photographer + /// named. Camera RGB is the body's own primaries: the same arc would + /// select a different set of colours on every make of sensor, and a skin + /// tone stored on one body would land on foliage on another. + Colour { + /// Centre of the accepted arc, in turns of the hue circle, + /// `0.0..1.0`. Wraps, so 0.98 and 0.02 are close together. + hue: f32, + /// Half-width of the arc, in turns. Bounded by [`MAX_HUE_WIDTH`]. + hue_width: f32, + /// Lower edge of the accepted chroma, `0.0..=1.0`, in the + /// max-minus-min sense `colour_saturation` uses. + chroma_lo: f32, + /// Upper edge of the accepted chroma. Kept above `chroma_lo` by + /// [`MaskSource::colour_range`]. + chroma_hi: f32, + /// The fade at every edge of the band — both ends of the arc and both + /// ends of the chroma range. One number rather than three: they are + /// the same control to a photographer, who is choosing how strictly + /// the selection is drawn, not tuning three of them against each + /// other. + softness: f32, + }, } impl MaskSource { @@ -663,6 +793,8 @@ impl MaskSource { Self::Linear { .. } => "linear", Self::Radial { .. } => "radial", Self::Brush { .. } => "brush", + Self::Luminance { .. } => "luminance", + Self::Colour { .. } => "colour", } } @@ -673,6 +805,82 @@ impl MaskSource { } } + /// TRACES: FR-DEV-10 + /// A luminance band, with the bounds put in order and inside the scale. + /// + /// **The only way one is built**, including on the way in from a sidecar. + /// The invariants a band has — bounds inside `0..1`, `lo` at or below + /// `hi`, softness not negative — are cheap to state once and impossible + /// to remember at four call sites. A crossed pair is the interesting one: + /// it comes from a user dragging the lower handle past the upper, and a + /// shader given it would select nothing at all rather than the thin band + /// the gesture plainly meant. + pub fn luminance_range(lo: f32, hi: f32, softness: f32) -> Self { + let (lo, hi) = ordered(lo, hi); + Self::Luminance { + lo, + hi, + softness: softness.clamp(0.0, 1.0), + } + } + + /// TRACES: FR-DEV-10 + /// A colour band, clamped and ordered like [`Self::luminance_range`]. + /// + /// The hue is wrapped rather than clamped, because the hue circle has no + /// ends: a control dragged past red comes back round to red, where + /// clamping would stick it there and make the far side of the circle + /// unreachable from one direction. + pub fn colour_range( + hue: f32, + hue_width: f32, + chroma_lo: f32, + chroma_hi: f32, + softness: f32, + ) -> Self { + let (chroma_lo, chroma_hi) = ordered(chroma_lo, chroma_hi); + Self::Colour { + hue: hue.rem_euclid(1.0), + hue_width: hue_width.clamp(0.0, MAX_HUE_WIDTH), + chroma_lo, + chroma_hi, + softness: softness.clamp(0.0, 1.0), + } + } + + /// TRACES: FR-DEV-10 + /// The band a new luminance layer starts on: the brighter half. + /// + /// Not the whole scale, which would select everything and read as a mask + /// that had failed to do anything. The upper half is what a photographer + /// reaches for first — holding back a sky, shaping a highlight — and it + /// is immediately visible on almost any photograph, so the control the + /// user then drags starts from something they can see. + pub fn highlights() -> Self { + Self::luminance_range(0.5, 1.0, DEFAULT_RANGE_SOFTNESS) + } + + /// TRACES: FR-DEV-10 + /// The band a new colour layer starts on: the orange–red arc skin sits in. + /// + /// Opinionated, and deliberately. Selecting by colour is a general tool + /// and skin is the reason most people reach for it, so starting there + /// means the first thing the user sees is the answer to the question they + /// asked; starting at red, or at whatever hue zero happens to be, would + /// mean the control had to be understood before it did anything. + pub fn skin_tones() -> Self { + Self::colour_range(0.06, 0.05, 0.15, 1.0, DEFAULT_RANGE_SOFTNESS) + } + + /// TRACES: FR-DEV-10 + /// Whether this mask selects by a property rather than by a shape. + /// + /// Asked by the panel, which offers a band's controls for one and a + /// shape's for the other, and by [`MaskLayer::is_stale`]. + pub fn is_range(&self) -> bool { + matches!(self, Self::Luminance { .. } | Self::Colour { .. }) + } + /// The strokes, or nothing for a source that is not painted. pub fn strokes(&self) -> &[Stroke] { match self { @@ -947,6 +1155,14 @@ impl MaskLayer { /// adjustment to the whole photograph before a single stroke was made — /// the loud, wrong-looking failure this codebase avoids everywhere else a /// mask can go missing. + /// + /// A range answers yes, and it is worth saying why rather than leaving it + /// to the catch-all. A band *can* be empty on a particular photograph — a + /// highlight band over a picture with no highlights in it — but nothing + /// on this side could know that without rasterising the mask, which is the + /// one thing this module promises never to do. So the honest answer is + /// that the layer selects whatever the picture has, and an empty result + /// is the picture's answer rather than the layer's. fn covers(&self) -> bool { match &self.source { MaskSource::Brush { strokes } => strokes.iter().any(|s| !s.erase && !s.is_empty()), @@ -985,9 +1201,16 @@ impl MaskLayer { // same thing whatever was or was not detected, so nothing about a // new run can invalidate it. Painted strokes are the same: they are // where the user put them, not where a model thought something was. - MaskSource::Linear { .. } | MaskSource::Radial { .. } | MaskSource::Brush { .. } => { - false - } + // + // A range is the strongest case of all: it is a band over the + // photograph's own values, so it does not merely survive a new + // segmentation — there is nothing a model could ever say that + // would change which pixels it names. + MaskSource::Linear { .. } + | MaskSource::Radial { .. } + | MaskSource::Brush { .. } + | MaskSource::Luminance { .. } + | MaskSource::Colour { .. } => false, } } @@ -1935,4 +2158,74 @@ mod tests { ); } } + + /// TRACES: FR-DEV-10 + /// A band the user dragged inside out is the band they described, not an + /// empty one. Dropping it to zero width would make the drag stop dead + /// under the finger at the moment the handles crossed. + #[test] + fn a_crossed_band_is_the_band_between_the_two_numbers() { + let MaskSource::Luminance { lo, hi, .. } = MaskSource::luminance_range(0.8, 0.3, 0.1) + else { + panic!("not a luminance range"); + }; + assert_eq!((lo, hi), (0.3, 0.8)); + } + + /// TRACES: FR-DEV-10 + /// The hue circle has no ends. Clamping a hue past red would stick it + /// there, and the far side of the circle would be unreachable from one + /// direction — a control that stops turning is a control that is broken. + #[test] + fn a_hue_past_the_end_of_the_circle_wraps() { + let MaskSource::Colour { hue, .. } = MaskSource::colour_range(1.25, 0.05, 0.1, 1.0, 0.1) + else { + panic!("not a colour range"); + }; + assert!((hue - 0.25).abs() < 1e-6, "hue wrapped to {hue}"); + + let MaskSource::Colour { hue, .. } = MaskSource::colour_range(-0.1, 0.05, 0.1, 1.0, 0.1) + else { + panic!("not a colour range"); + }; + assert!((hue - 0.9).abs() < 1e-6, "hue wrapped to {hue}"); + } + + /// TRACES: FR-DEV-10 + /// An arc may not quietly become the whole circle. Half a turn each way is + /// every colour there is, arrived at through a control the photographer + /// believed was narrowing something. + #[test] + fn a_hue_arc_cannot_swallow_the_circle() { + let MaskSource::Colour { hue_width, .. } = + MaskSource::colour_range(0.0, 9.0, 0.1, 1.0, 0.1) + else { + panic!("not a colour range"); + }; + assert_eq!(hue_width, MAX_HUE_WIDTH); + } + + /// TRACES: FR-DEV-10 + /// Nothing a model finds can change which pixels a band names, so a range + /// layer is never stale — where a subject layer read against a new + /// segmentation is exactly that. + #[test] + fn a_range_never_goes_stale() { + assert!(!MaskLayer::new("m1", MaskSource::highlights()).is_stale(7)); + assert!(!MaskLayer::new("m2", MaskSource::skin_tones()).is_stale(7)); + } + + /// TRACES: FR-DEV-10 + /// A range is a rule, and the rule is the whole of what it stores. + #[test] + fn a_range_is_a_rule_and_not_a_shape() { + let source = MaskSource::skin_tones(); + assert!(source.is_range()); + assert!( + source.strokes().is_empty(), + "a range has no geometry to hand out" + ); + assert_eq!(source.kind(), "colour"); + assert_eq!(MaskSource::highlights().kind(), "luminance"); + } } diff --git a/core/dr-pipeline/src/sidecar.rs b/core/dr-pipeline/src/sidecar.rs index 2eaa5fc..af9c763 100644 --- a/core/dr-pipeline/src/sidecar.rs +++ b/core/dr-pipeline/src/sidecar.rs @@ -1129,6 +1129,46 @@ fn write_mask(out: &mut String, version: &str, layer: &MaskLayer) { let _ = writeln!(out, "feather = {}", format_value(*feather)); } MaskSource::Brush { strokes } => write_strokes(out, strokes), + // TRACES: FR-DEV-10 + // `band` rather than a key per edge, and the same key for both range + // sources. The two bounds and their softness are one quantity to a + // reader — "the band this mask selects" — and splitting them over + // three lines makes a hand edit that moves one edge and forgets the + // other look like a file that was written that way. + MaskSource::Luminance { lo, hi, softness } => { + let _ = writeln!( + out, + "band = {} {} {}", + format_value(*lo), + format_value(*hi), + format_value(*softness) + ); + } + MaskSource::Colour { + hue, + hue_width, + chroma_lo, + chroma_hi, + softness, + } => { + // The chroma half goes out as `band` so that the two range + // sources share a key for the same idea, and the arc gets its + // own: a hue is a position on a circle and its neighbours are not + // bounds in the sense the chroma pair are. + let _ = writeln!( + out, + "hue = {} {}", + format_value(*hue), + format_value(*hue_width) + ); + let _ = writeln!( + out, + "band = {} {} {}", + format_value(*chroma_lo), + format_value(*chroma_hi), + format_value(*softness) + ); + } } if layer.invert { @@ -1277,6 +1317,16 @@ struct PartialMask { angle: f32, width: f32, feather: f32, + /// TRACES: FR-DEV-10 + /// A range source's band: its two bounds and the fade at each edge. + /// + /// One triple for both range kinds — tone positions for a luminance + /// layer, chroma for a colour one — because they are the same line in the + /// file and reading them into two sets of fields would be two ways to + /// spell one thing. + band: (f32, f32, f32), + /// A colour range's arc: centre and half-width, in turns. + hue: (f32, f32), invert: bool, opacity: f32, enabled: bool, @@ -1311,6 +1361,11 @@ impl PartialMask { angle: 0.0, width: 0.0, feather: 0.0, + // The defaults a new layer gets, so a block that names a range + // source and nothing else reads back as the mask the panel would + // have made rather than as an empty band selecting nothing. + band: (0.5, 1.0, crate::mask::DEFAULT_RANGE_SOFTNESS), + hue: (0.06, 0.05), invert: false, opacity: 1.0, enabled: true, @@ -1356,6 +1411,12 @@ impl PartialMask { "angle" => self.angle = value.parse().unwrap_or(0.0), "width" => self.width = value.parse().unwrap_or(0.0), "feather" => self.feather = value.parse().unwrap_or(0.0), + // TRACES: FR-DEV-10 + // Kept as written and ordered later, in `MaskSource::*_range`. + // Clamping here as well would be a second place the band's + // invariants live, and the two would drift. + "band" => self.band = triple(value).unwrap_or(self.band), + "hue" => self.hue = pair(value).unwrap_or(self.hue), // A cache, so an unreadable one is dropped rather than refused: // the layer still says what it selects, and the worst a `None` // here costs is a model run. Refusing the layer over it would @@ -1454,6 +1515,18 @@ impl PartialMask { "brush" => MaskSource::Brush { strokes: self.strokes, }, + // TRACES: FR-DEV-10 + // Through the constructors, not built by hand: they are where the + // band's invariants are kept, and a file is exactly the input + // that has not been through them. + "luminance" => MaskSource::luminance_range(self.band.0, self.band.1, self.band.2), + "colour" => MaskSource::colour_range( + self.hue.0, + self.hue.1, + self.band.0, + self.band.1, + self.band.2, + ), other => { log::warn!( "sidecar: unknown mask source '{other}'; skipping layer {}", @@ -1517,9 +1590,23 @@ impl PartialMask { /// Two whitespace-separated floats. fn pair(value: &str) -> Option<(f32, f32)> { let mut it = value.split_whitespace(); - let a = it.next()?.parse().ok()?; - let b = it.next()?.parse().ok()?; - Some((a, b)) + let a: f32 = it.next()?.parse().ok()?; + let b: f32 = it.next()?.parse().ok()?; + // A NaN parses — `"nan"` is a valid float — and then survives every clamp + // downstream, because every comparison against it is false. It reaches a + // uniform, and a mask whose geometry or band is NaN selects nothing while + // looking, in the file and in the panel, exactly like one that should. + (a.is_finite() && b.is_finite()).then_some((a, b)) +} + +/// TRACES: FR-DEV-10 +/// Three whitespace-separated floats — a range mask's band. +fn triple(value: &str) -> Option<(f32, f32, f32)> { + let mut it = value.split_whitespace(); + let a: f32 = it.next()?.parse().ok()?; + let b: f32 = it.next()?.parse().ok()?; + let c: f32 = it.next()?.parse().ok()?; + (a.is_finite() && b.is_finite() && c.is_finite()).then_some((a, b, c)) } /// Format a value without a trailing `.0` on whole numbers, and without diff --git a/core/dr-pipeline/tests/mask_sidecar.rs b/core/dr-pipeline/tests/mask_sidecar.rs index d0dbec4..0204f8d 100644 --- a/core/dr-pipeline/tests/mask_sidecar.rs +++ b/core/dr-pipeline/tests/mask_sidecar.rs @@ -1124,3 +1124,110 @@ fn a_real_edit_is_still_a_conflict_with_coverage_present() { vec![("mask".to_string(), "m1".to_string())] ); } + +// --------------------------------------------------------------------------- +// Range masks (FR-DEV-10) +// --------------------------------------------------------------------------- + +/// TRACES: FR-DEV-10 +/// A band is the whole of what a range mask is, so losing one edge of it is +/// losing the mask — and losing it silently, because the layer still names a +/// source and still draws something. +#[test] +fn range_masks_keep_their_bands() { + let mut graph = EditGraph::default_chain(); + + let mut tone = MaskLayer::new("m1", MaskSource::luminance_range(0.62, 0.91, 0.2)); + tone.set_param("exposure", ParamId("exposure"), -0.75); + graph.masks_mut().push(tone); + + let mut colour = MaskLayer::new("m2", MaskSource::colour_range(0.07, 0.04, 0.2, 0.85, 0.11)); + colour.set_param("exposure", ParamId("exposure"), 0.25); + graph.masks_mut().push(colour); + + let restored = round_trip(&graph); + let layers = restored.masks().layers(); + assert_eq!(layers.len(), 2); + assert_eq!( + layers[0].source, + MaskSource::luminance_range(0.62, 0.91, 0.2) + ); + assert_eq!( + layers[1].source, + MaskSource::colour_range(0.07, 0.04, 0.2, 0.85, 0.11) + ); +} + +/// TRACES: FR-DEV-10 +/// Two devices that made the same range mask must write the same bytes, or +/// per-field merge (FR-NC-9) has a difference to arbitrate that is not one. +#[test] +fn writing_a_range_mask_twice_is_byte_identical() { + let mut graph = EditGraph::default_chain(); + let mut layer = MaskLayer::new("m1", MaskSource::skin_tones()); + layer.set_param("exposure", ParamId("exposure"), 0.4); + graph.masks_mut().push(layer); + + let mut sidecar = Sidecar::new(); + sidecar.put(Version::from_graph("default", "Default", &graph)); + + let once = sidecar.to_text(); + let twice = Sidecar::parse(&once).expect("reparse").to_text(); + assert_eq!(once, twice); +} + +/// TRACES: FR-DEV-10 +/// A band written the wrong way round — by hand, or by a build that let a +/// drag cross the handles — is a mask that selects nothing. Ordering it on +/// read is what makes that a recoverable file rather than a lost edit. +#[test] +fn a_crossed_band_is_ordered_on_read() { + let text = "drsc 1\n\ + \n[version default]\n\ + name = Default\n\ + \n[mask default m1]\n\ + source = luminance\n\ + band = 0.9 0.2 0.1\n\ + exposure.exposure = 0.5\n"; + + let parsed = Sidecar::parse(text).expect("parse"); + let mut graph = EditGraph::default_chain(); + parsed + .versions + .get("default") + .expect("version") + .apply(&mut graph) + .expect_no_film(); + + assert_eq!( + graph.masks().layers()[0].source, + MaskSource::luminance_range(0.2, 0.9, 0.1), + "the band came back as the one the numbers describe" + ); +} + +/// TRACES: FR-DEV-10 +/// A range block with no band at all is a file from a build that spelled the +/// key differently, or a hand edit that deleted a line. It reads back as the +/// mask the panel would have made rather than as an empty selection, because +/// an empty selection looks exactly like a mask that works and does nothing. +#[test] +fn a_range_mask_with_no_band_falls_back_to_the_default_one() { + let text = "drsc 1\n\ + \n[version default]\n\ + name = Default\n\ + \n[mask default m1]\n\ + source = luminance\n\ + exposure.exposure = 0.5\n"; + + let parsed = Sidecar::parse(text).expect("parse"); + let mut graph = EditGraph::default_chain(); + parsed + .versions + .get("default") + .expect("version") + .apply(&mut graph) + .expect_no_film(); + + assert_eq!(graph.masks().layers()[0].source, MaskSource::highlights()); +} diff --git a/docs/requirements.md b/docs/requirements.md index 45d6561..6b730a7 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -473,7 +473,6 @@ and the subject, as a develop operation with a single symmetric amount. The tran estimated from the photograph rather than supplied by the photographer, and every length the estimate depends on is stored as a fraction of the frame, so that what is judged on screen is what lands in the exported file (FR-DSP-1). - Haze is the one degradation the tone and colour controls cannot reach, because it is spatially varying: a black point that clears the mountains crushes the foreground, and a contrast curve that clears the mountains does the same. It belongs with texture and clarity in FR-DEV-3's adjustment @@ -481,6 +480,15 @@ set as a member of the compositional detail family — the operations whose radi the picture rather than of the sensor — and it runs in the same neighbourhood stage, for the same reason: it is defined by what the pixels around a pixel are doing. +**FR-DEV-10 — Range masks.** A mask layer may select by a band over the photograph's own values — +luminance, or colour — with soft edges, stored as bounds and softness rather than as pixels and +rasterised on the device in the existing mask pass. +Every mask in FR-DEV-3 begins from a shape: painted, drawn, or found by a model. A range begins +from a property, which is what a luminosity mask has always been and what makes a local adjustment +blend without a halo — the boundary is the picture's own, at whatever detail the picture has. It is +also the only route to selecting by skin tone, and the prerequisite for composing a selection from +several criteria at once. + ### 3.4 Display and interaction **FR-DSP-1 — Proxy-resolution rendering.** The develop view renders at the resolution actually diff --git a/docs/traceability.md b/docs/traceability.md index aec986b..e8a9e09 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,17 +9,17 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 339 | -| TRACES tags found | 1129 | -| Requirements defined | 182 | -| Requirements covered | 129 | -| **Coverage** | **70.9%** (129/182) | +| Source files scanned | 337 | +| TRACES tags found | 1175 | +| Requirements defined | 181 | +| Requirements covered | 128 | +| **Coverage** | **70.7%** (128/181) | ### By type | Type | Covered | Defined | |---|---|---| -| FR | 96 | 127 | +| FR | 95 | 126 | | NFR | 30 | 49 | | R | 3 | 6 | @@ -45,14 +45,14 @@ _None._ | FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1231`](../core/dr-catalog/src/schema.rs#L1231), [`core/dr-catalog/src/schema.rs:728`](../core/dr-catalog/src/schema.rs#L728), [`core/dr-decode/src/lib.rs:285`](../core/dr-decode/src/lib.rs#L285), [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/collections_ui.rs:1579`](../ui/dr-ui/src/collections_ui.rs#L1579), [`ui/dr-ui/src/collections_ui.rs:1685`](../ui/dr-ui/src/collections_ui.rs#L1685), [`ui/dr-ui/src/collections_ui.rs:1734`](../ui/dr-ui/src/collections_ui.rs#L1734), [`ui/dr-ui/src/collections_ui.rs:238`](../ui/dr-ui/src/collections_ui.rs#L238), [`ui/dr-ui/src/collections_ui.rs:335`](../ui/dr-ui/src/collections_ui.rs#L335), [`ui/dr-ui/src/collections_ui.rs:87`](../ui/dr-ui/src/collections_ui.rs#L87), [`ui/dr-ui/src/library.rs:4488`](../ui/dr-ui/src/library.rs#L4488), [`ui/dr-ui/src/library_ui.rs:713`](../ui/dr-ui/src/library_ui.rs#L713), [`ui/dr-ui/src/library_ui.rs:7691`](../ui/dr-ui/src/library_ui.rs#L7691), [`ui/dr-ui/src/library_ui.rs:7702`](../ui/dr-ui/src/library_ui.rs#L7702), [`ui/dr-ui/src/library_ui.rs:7715`](../ui/dr-ui/src/library_ui.rs#L7715), [`ui/dr-ui/src/library_ui.rs:7730`](../ui/dr-ui/src/library_ui.rs#L7730), [`ui/dr-ui/src/library_ui.rs:7739`](../ui/dr-ui/src/library_ui.rs#L7739), [`ui/dr-ui/ui/app.slint:355`](../ui/dr-ui/ui/app.slint#L355), [`ui/dr-ui/ui/app.slint:577`](../ui/dr-ui/ui/app.slint#L577), [`ui/dr-ui/ui/library.slint:1350`](../ui/dr-ui/ui/library.slint#L1350), [`ui/dr-ui/ui/library.slint:1374`](../ui/dr-ui/ui/library.slint#L1374), [`ui/dr-ui/ui/library.slint:1427`](../ui/dr-ui/ui/library.slint#L1427), [`ui/dr-ui/ui/library.slint:19`](../ui/dr-ui/ui/library.slint#L19), [`ui/dr-ui/ui/library.slint:4101`](../ui/dr-ui/ui/library.slint#L4101) | | FR-CAT-6 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/query.rs:1`](../core/dr-catalog/src/query.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:728`](../core/dr-catalog/src/schema.rs#L728), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:64`](../core/dr-types/src/settings.rs#L64), [`core/dr-types/src/time.rs:67`](../core/dr-types/src/time.rs#L67), [`core/dr-types/src/time.rs:90`](../core/dr-types/src/time.rs#L90), [`ui/dr-ui/src/library.rs:254`](../ui/dr-ui/src/library.rs#L254), [`ui/dr-ui/src/library.rs:4964`](../ui/dr-ui/src/library.rs#L4964), [`ui/dr-ui/src/library_ui.rs:389`](../ui/dr-ui/src/library_ui.rs#L389), [`ui/dr-ui/src/library_ui.rs:470`](../ui/dr-ui/src/library_ui.rs#L470), [`ui/dr-ui/src/library_ui.rs:6421`](../ui/dr-ui/src/library_ui.rs#L6421), [`ui/dr-ui/src/library_ui.rs:6474`](../ui/dr-ui/src/library_ui.rs#L6474), [`ui/dr-ui/src/library_ui.rs:7831`](../ui/dr-ui/src/library_ui.rs#L7831), [`ui/dr-ui/ui/app.slint:367`](../ui/dr-ui/ui/app.slint#L367), [`ui/dr-ui/ui/app.slint:577`](../ui/dr-ui/ui/app.slint#L577), [`ui/dr-ui/ui/app.slint:892`](../ui/dr-ui/ui/app.slint#L892), [`ui/dr-ui/ui/library.slint:111`](../ui/dr-ui/ui/library.slint#L111), [`ui/dr-ui/ui/library.slint:1537`](../ui/dr-ui/ui/library.slint#L1537), [`ui/dr-ui/ui/library.slint:4101`](../ui/dr-ui/ui/library.slint#L4101), [`ui/dr-ui/ui/settings.slint:158`](../ui/dr-ui/ui/settings.slint#L158) | | FR-CAT-7 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1073`](../ui/dr-ui/src/collections_ui.rs#L1073), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1598`](../ui/dr-ui/src/collections_ui.rs#L1598), [`ui/dr-ui/src/collections_ui.rs:1848`](../ui/dr-ui/src/collections_ui.rs#L1848), [`ui/dr-ui/src/collections_ui.rs:1913`](../ui/dr-ui/src/collections_ui.rs#L1913), [`ui/dr-ui/src/collections_ui.rs:1973`](../ui/dr-ui/src/collections_ui.rs#L1973), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/collections_ui.rs:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:365`](../ui/dr-ui/src/collections_ui.rs#L365), [`ui/dr-ui/src/collections_ui.rs:569`](../ui/dr-ui/src/collections_ui.rs#L569), [`ui/dr-ui/src/collections_ui.rs:965`](../ui/dr-ui/src/collections_ui.rs#L965), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:4769`](../ui/dr-ui/src/library.rs#L4769), [`ui/dr-ui/src/library.rs:4795`](../ui/dr-ui/src/library.rs#L4795), [`ui/dr-ui/src/library.rs:4850`](../ui/dr-ui/src/library.rs#L4850), [`ui/dr-ui/src/library_ui.rs:2700`](../ui/dr-ui/src/library_ui.rs#L2700), [`ui/dr-ui/src/library_ui.rs:4097`](../ui/dr-ui/src/library_ui.rs#L4097), [`ui/dr-ui/src/library_ui.rs:4848`](../ui/dr-ui/src/library_ui.rs#L4848), [`ui/dr-ui/ui/app.slint:358`](../ui/dr-ui/ui/app.slint#L358), [`ui/dr-ui/ui/app.slint:572`](../ui/dr-ui/ui/app.slint#L572), [`ui/dr-ui/ui/app.slint:601`](../ui/dr-ui/ui/app.slint#L601), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:1317`](../ui/dr-ui/ui/library.slint#L1317), [`ui/dr-ui/ui/library.slint:2886`](../ui/dr-ui/ui/library.slint#L2886), [`ui/dr-ui/ui/library.slint:4079`](../ui/dr-ui/ui/library.slint#L4079), [`ui/dr-ui/ui/library.slint:993`](../ui/dr-ui/ui/library.slint#L993) | -| FR-CAT-8 | [`core/dr-catalog/src/schema.rs:532`](../core/dr-catalog/src/schema.rs#L532), [`core/dr-pipeline/src/coverage.rs:1`](../core/dr-pipeline/src/coverage.rs#L1), [`core/dr-pipeline/src/graph.rs:465`](../core/dr-pipeline/src/graph.rs#L465), [`core/dr-pipeline/src/graph.rs:513`](../core/dr-pipeline/src/graph.rs#L513), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1165`](../core/dr-pipeline/src/sidecar.rs#L1165), [`core/dr-pipeline/src/sidecar.rs:1831`](../core/dr-pipeline/src/sidecar.rs#L1831), [`core/dr-pipeline/src/sidecar.rs:94`](../core/dr-pipeline/src/sidecar.rs#L94), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:45`](../core/dr-sync/src/scan.rs#L45), [`core/dr-sync/src/scan.rs:912`](../core/dr-sync/src/scan.rs#L912), [`ui/dr-ui/src/develop.rs:1933`](../ui/dr-ui/src/develop.rs#L1933), [`ui/dr-ui/src/develop.rs:4265`](../ui/dr-ui/src/develop.rs#L4265), [`ui/dr-ui/src/develop.rs:4294`](../ui/dr-ui/src/develop.rs#L4294), [`ui/dr-ui/src/develop.rs:5214`](../ui/dr-ui/src/develop.rs#L5214), [`ui/dr-ui/src/develop.rs:5268`](../ui/dr-ui/src/develop.rs#L5268), [`ui/dr-ui/src/develop.rs:5370`](../ui/dr-ui/src/develop.rs#L5370), [`ui/dr-ui/src/export.rs:799`](../ui/dr-ui/src/export.rs#L799), [`ui/dr-ui/src/lib.rs:1082`](../ui/dr-ui/src/lib.rs#L1082), [`ui/dr-ui/src/lib.rs:1598`](../ui/dr-ui/src/lib.rs#L1598), [`ui/dr-ui/src/lib.rs:2102`](../ui/dr-ui/src/lib.rs#L2102), [`ui/dr-ui/src/lib.rs:2237`](../ui/dr-ui/src/lib.rs#L2237), [`ui/dr-ui/src/lib.rs:634`](../ui/dr-ui/src/lib.rs#L634), [`ui/dr-ui/src/library.rs:1376`](../ui/dr-ui/src/library.rs#L1376), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library.rs:2219`](../ui/dr-ui/src/library.rs#L2219), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500), [`ui/dr-ui/src/library.rs:547`](../ui/dr-ui/src/library.rs#L547), [`ui/dr-ui/src/library.rs:584`](../ui/dr-ui/src/library.rs#L584), [`ui/dr-ui/src/library.rs:7144`](../ui/dr-ui/src/library.rs#L7144), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:845`](../ui/dr-ui/src/library.rs#L845), [`ui/dr-ui/src/library_ui.rs:1265`](../ui/dr-ui/src/library_ui.rs#L1265), [`ui/dr-ui/src/library_ui.rs:6057`](../ui/dr-ui/src/library_ui.rs#L6057), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | -| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:785`](../core/dr-catalog/src/schema.rs#L785), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:200`](../core/dr-sync/src/scan.rs#L200), [`core/dr-sync/src/scan.rs:505`](../core/dr-sync/src/scan.rs#L505), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`ui/dr-ui/src/develop.rs:3652`](../ui/dr-ui/src/develop.rs#L3652), [`ui/dr-ui/src/develop.rs:3695`](../ui/dr-ui/src/develop.rs#L3695), [`ui/dr-ui/src/library.rs:1276`](../ui/dr-ui/src/library.rs#L1276), [`ui/dr-ui/src/library.rs:1329`](../ui/dr-ui/src/library.rs#L1329), [`ui/dr-ui/src/library.rs:1360`](../ui/dr-ui/src/library.rs#L1360), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:1727`](../ui/dr-ui/src/library.rs#L1727), [`ui/dr-ui/src/library.rs:2179`](../ui/dr-ui/src/library.rs#L2179), [`ui/dr-ui/src/library.rs:2255`](../ui/dr-ui/src/library.rs#L2255), [`ui/dr-ui/src/library.rs:274`](../ui/dr-ui/src/library.rs#L274), [`ui/dr-ui/src/library.rs:5072`](../ui/dr-ui/src/library.rs#L5072), [`ui/dr-ui/src/library.rs:584`](../ui/dr-ui/src/library.rs#L584), [`ui/dr-ui/src/library.rs:829`](../ui/dr-ui/src/library.rs#L829), [`ui/dr-ui/src/library.rs:845`](../ui/dr-ui/src/library.rs#L845), [`ui/dr-ui/src/library.rs:899`](../ui/dr-ui/src/library.rs#L899), [`ui/dr-ui/src/library_ui.rs:1332`](../ui/dr-ui/src/library_ui.rs#L1332), [`ui/dr-ui/src/library_ui.rs:1923`](../ui/dr-ui/src/library_ui.rs#L1923), [`ui/dr-ui/src/library_ui.rs:1967`](../ui/dr-ui/src/library_ui.rs#L1967), [`ui/dr-ui/src/library_ui.rs:1983`](../ui/dr-ui/src/library_ui.rs#L1983), [`ui/dr-ui/src/library_ui.rs:2077`](../ui/dr-ui/src/library_ui.rs#L2077), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:2939`](../ui/dr-ui/src/library_ui.rs#L2939), [`ui/dr-ui/src/library_ui.rs:317`](../ui/dr-ui/src/library_ui.rs#L317), [`ui/dr-ui/src/library_ui.rs:325`](../ui/dr-ui/src/library_ui.rs#L325), [`ui/dr-ui/src/library_ui.rs:3377`](../ui/dr-ui/src/library_ui.rs#L3377), [`ui/dr-ui/src/library_ui.rs:3599`](../ui/dr-ui/src/library_ui.rs#L3599), [`ui/dr-ui/src/library_ui.rs:3877`](../ui/dr-ui/src/library_ui.rs#L3877), [`ui/dr-ui/src/library_ui.rs:3965`](../ui/dr-ui/src/library_ui.rs#L3965), [`ui/dr-ui/src/library_ui.rs:4148`](../ui/dr-ui/src/library_ui.rs#L4148), [`ui/dr-ui/src/library_ui.rs:4262`](../ui/dr-ui/src/library_ui.rs#L4262), [`ui/dr-ui/src/library_ui.rs:523`](../ui/dr-ui/src/library_ui.rs#L523), [`ui/dr-ui/src/library_ui.rs:586`](../ui/dr-ui/src/library_ui.rs#L586), [`ui/dr-ui/src/library_ui.rs:6519`](../ui/dr-ui/src/library_ui.rs#L6519), [`ui/dr-ui/src/library_ui.rs:6634`](../ui/dr-ui/src/library_ui.rs#L6634), [`ui/dr-ui/src/presets.rs:468`](../ui/dr-ui/src/presets.rs#L468), [`ui/dr-ui/src/presets.rs:480`](../ui/dr-ui/src/presets.rs#L480), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | +| FR-CAT-8 | [`core/dr-catalog/src/schema.rs:532`](../core/dr-catalog/src/schema.rs#L532), [`core/dr-pipeline/src/coverage.rs:1`](../core/dr-pipeline/src/coverage.rs#L1), [`core/dr-pipeline/src/graph.rs:465`](../core/dr-pipeline/src/graph.rs#L465), [`core/dr-pipeline/src/graph.rs:513`](../core/dr-pipeline/src/graph.rs#L513), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1205`](../core/dr-pipeline/src/sidecar.rs#L1205), [`core/dr-pipeline/src/sidecar.rs:1918`](../core/dr-pipeline/src/sidecar.rs#L1918), [`core/dr-pipeline/src/sidecar.rs:94`](../core/dr-pipeline/src/sidecar.rs#L94), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:45`](../core/dr-sync/src/scan.rs#L45), [`core/dr-sync/src/scan.rs:912`](../core/dr-sync/src/scan.rs#L912), [`ui/dr-ui/src/develop.rs:1933`](../ui/dr-ui/src/develop.rs#L1933), [`ui/dr-ui/src/develop.rs:4415`](../ui/dr-ui/src/develop.rs#L4415), [`ui/dr-ui/src/develop.rs:4444`](../ui/dr-ui/src/develop.rs#L4444), [`ui/dr-ui/src/develop.rs:5364`](../ui/dr-ui/src/develop.rs#L5364), [`ui/dr-ui/src/develop.rs:5418`](../ui/dr-ui/src/develop.rs#L5418), [`ui/dr-ui/src/develop.rs:5520`](../ui/dr-ui/src/develop.rs#L5520), [`ui/dr-ui/src/export.rs:799`](../ui/dr-ui/src/export.rs#L799), [`ui/dr-ui/src/lib.rs:1082`](../ui/dr-ui/src/lib.rs#L1082), [`ui/dr-ui/src/lib.rs:1598`](../ui/dr-ui/src/lib.rs#L1598), [`ui/dr-ui/src/lib.rs:2102`](../ui/dr-ui/src/lib.rs#L2102), [`ui/dr-ui/src/lib.rs:2237`](../ui/dr-ui/src/lib.rs#L2237), [`ui/dr-ui/src/lib.rs:634`](../ui/dr-ui/src/lib.rs#L634), [`ui/dr-ui/src/library.rs:1376`](../ui/dr-ui/src/library.rs#L1376), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library.rs:2219`](../ui/dr-ui/src/library.rs#L2219), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500), [`ui/dr-ui/src/library.rs:547`](../ui/dr-ui/src/library.rs#L547), [`ui/dr-ui/src/library.rs:584`](../ui/dr-ui/src/library.rs#L584), [`ui/dr-ui/src/library.rs:7144`](../ui/dr-ui/src/library.rs#L7144), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:845`](../ui/dr-ui/src/library.rs#L845), [`ui/dr-ui/src/library_ui.rs:1265`](../ui/dr-ui/src/library_ui.rs#L1265), [`ui/dr-ui/src/library_ui.rs:6057`](../ui/dr-ui/src/library_ui.rs#L6057), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-CAT-9 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/schema.rs:785`](../core/dr-catalog/src/schema.rs#L785), [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-catalog/src/walk.rs:728`](../core/dr-catalog/src/walk.rs#L728), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-sync/src/scan.rs:200`](../core/dr-sync/src/scan.rs#L200), [`core/dr-sync/src/scan.rs:505`](../core/dr-sync/src/scan.rs#L505), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`ui/dr-ui/src/develop.rs:3802`](../ui/dr-ui/src/develop.rs#L3802), [`ui/dr-ui/src/develop.rs:3845`](../ui/dr-ui/src/develop.rs#L3845), [`ui/dr-ui/src/library.rs:1276`](../ui/dr-ui/src/library.rs#L1276), [`ui/dr-ui/src/library.rs:1329`](../ui/dr-ui/src/library.rs#L1329), [`ui/dr-ui/src/library.rs:1360`](../ui/dr-ui/src/library.rs#L1360), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:1727`](../ui/dr-ui/src/library.rs#L1727), [`ui/dr-ui/src/library.rs:2179`](../ui/dr-ui/src/library.rs#L2179), [`ui/dr-ui/src/library.rs:2255`](../ui/dr-ui/src/library.rs#L2255), [`ui/dr-ui/src/library.rs:274`](../ui/dr-ui/src/library.rs#L274), [`ui/dr-ui/src/library.rs:5072`](../ui/dr-ui/src/library.rs#L5072), [`ui/dr-ui/src/library.rs:584`](../ui/dr-ui/src/library.rs#L584), [`ui/dr-ui/src/library.rs:829`](../ui/dr-ui/src/library.rs#L829), [`ui/dr-ui/src/library.rs:845`](../ui/dr-ui/src/library.rs#L845), [`ui/dr-ui/src/library.rs:899`](../ui/dr-ui/src/library.rs#L899), [`ui/dr-ui/src/library_ui.rs:1332`](../ui/dr-ui/src/library_ui.rs#L1332), [`ui/dr-ui/src/library_ui.rs:1923`](../ui/dr-ui/src/library_ui.rs#L1923), [`ui/dr-ui/src/library_ui.rs:1967`](../ui/dr-ui/src/library_ui.rs#L1967), [`ui/dr-ui/src/library_ui.rs:1983`](../ui/dr-ui/src/library_ui.rs#L1983), [`ui/dr-ui/src/library_ui.rs:2077`](../ui/dr-ui/src/library_ui.rs#L2077), [`ui/dr-ui/src/library_ui.rs:284`](../ui/dr-ui/src/library_ui.rs#L284), [`ui/dr-ui/src/library_ui.rs:2939`](../ui/dr-ui/src/library_ui.rs#L2939), [`ui/dr-ui/src/library_ui.rs:317`](../ui/dr-ui/src/library_ui.rs#L317), [`ui/dr-ui/src/library_ui.rs:325`](../ui/dr-ui/src/library_ui.rs#L325), [`ui/dr-ui/src/library_ui.rs:3377`](../ui/dr-ui/src/library_ui.rs#L3377), [`ui/dr-ui/src/library_ui.rs:3599`](../ui/dr-ui/src/library_ui.rs#L3599), [`ui/dr-ui/src/library_ui.rs:3877`](../ui/dr-ui/src/library_ui.rs#L3877), [`ui/dr-ui/src/library_ui.rs:3965`](../ui/dr-ui/src/library_ui.rs#L3965), [`ui/dr-ui/src/library_ui.rs:4148`](../ui/dr-ui/src/library_ui.rs#L4148), [`ui/dr-ui/src/library_ui.rs:4262`](../ui/dr-ui/src/library_ui.rs#L4262), [`ui/dr-ui/src/library_ui.rs:523`](../ui/dr-ui/src/library_ui.rs#L523), [`ui/dr-ui/src/library_ui.rs:586`](../ui/dr-ui/src/library_ui.rs#L586), [`ui/dr-ui/src/library_ui.rs:6519`](../ui/dr-ui/src/library_ui.rs#L6519), [`ui/dr-ui/src/library_ui.rs:6634`](../ui/dr-ui/src/library_ui.rs#L6634), [`ui/dr-ui/src/presets.rs:468`](../ui/dr-ui/src/presets.rs#L468), [`ui/dr-ui/src/presets.rs:480`](../ui/dr-ui/src/presets.rs#L480), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | | FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) | -| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:395`](../core/dr-catalog/src/schema.rs#L395), [`core/dr-catalog/src/schema.rs:614`](../core/dr-catalog/src/schema.rs#L614), [`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:117`](../core/dr-types/src/settings.rs#L117), [`ui/dr-ui/src/develop.rs:121`](../ui/dr-ui/src/develop.rs#L121), [`ui/dr-ui/src/develop.rs:130`](../ui/dr-ui/src/develop.rs#L130), [`ui/dr-ui/src/develop.rs:196`](../ui/dr-ui/src/develop.rs#L196), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/develop.rs:689`](../ui/dr-ui/src/develop.rs#L689), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2209`](../ui/dr-ui/src/lib.rs#L2209), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | +| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:395`](../core/dr-catalog/src/schema.rs#L395), [`core/dr-catalog/src/schema.rs:614`](../core/dr-catalog/src/schema.rs#L614), [`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:117`](../core/dr-types/src/settings.rs#L117), [`ui/dr-ui/src/develop.rs:121`](../ui/dr-ui/src/develop.rs#L121), [`ui/dr-ui/src/develop.rs:130`](../ui/dr-ui/src/develop.rs#L130), [`ui/dr-ui/src/develop.rs:196`](../ui/dr-ui/src/develop.rs#L196), [`ui/dr-ui/src/develop.rs:2178`](../ui/dr-ui/src/develop.rs#L2178), [`ui/dr-ui/src/develop.rs:689`](../ui/dr-ui/src/develop.rs#L689), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2209`](../ui/dr-ui/src/lib.rs#L2209), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-11 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:614`](../core/dr-catalog/src/schema.rs#L614), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/library.rs:295`](../ui/dr-ui/src/library.rs#L295), [`ui/dr-ui/src/library.rs:325`](../ui/dr-ui/src/library.rs#L325), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-12 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:395`](../core/dr-catalog/src/schema.rs#L395), [`core/dr-catalog/src/schema.rs:614`](../core/dr-catalog/src/schema.rs#L614), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) | | FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:148`](../core/dr-decode/src/preview.rs#L148), [`ui/dr-ui/src/import.rs:463`](../ui/dr-ui/src/import.rs#L463) | -| FR-CULL-3 | [`core/dr-gpu/src/focus.rs:154`](../core/dr-gpu/src/focus.rs#L154), [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/raw_histogram.rs:129`](../core/dr-gpu/src/raw_histogram.rs#L129), [`core/dr-gpu/src/raw_histogram.rs:1`](../core/dr-gpu/src/raw_histogram.rs#L1), [`core/dr-gpu/src/raw_histogram.rs:272`](../core/dr-gpu/src/raw_histogram.rs#L272), [`core/dr-gpu/src/raw_histogram.rs:407`](../core/dr-gpu/src/raw_histogram.rs#L407), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`core/dr-gpu/src/shaders/raw_histogram.wgsl:1`](../core/dr-gpu/src/shaders/raw_histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:3261`](../ui/dr-ui/src/develop.rs#L3261), [`ui/dr-ui/src/develop.rs:3274`](../ui/dr-ui/src/develop.rs#L3274), [`ui/dr-ui/src/develop.rs:3319`](../ui/dr-ui/src/develop.rs#L3319), [`ui/dr-ui/src/develop.rs:3330`](../ui/dr-ui/src/develop.rs#L3330), [`ui/dr-ui/src/develop.rs:3336`](../ui/dr-ui/src/develop.rs#L3336), [`ui/dr-ui/src/develop.rs:3353`](../ui/dr-ui/src/develop.rs#L3353), [`ui/dr-ui/src/develop.rs:6919`](../ui/dr-ui/src/develop.rs#L6919), [`ui/dr-ui/src/develop.rs:6983`](../ui/dr-ui/src/develop.rs#L6983), [`ui/dr-ui/src/develop.rs:7006`](../ui/dr-ui/src/develop.rs#L7006), [`ui/dr-ui/src/develop.rs:763`](../ui/dr-ui/src/develop.rs#L763), [`ui/dr-ui/src/develop.rs:783`](../ui/dr-ui/src/develop.rs#L783), [`ui/dr-ui/src/develop.rs:789`](../ui/dr-ui/src/develop.rs#L789), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/histogram.rs:208`](../ui/dr-ui/src/histogram.rs#L208), [`ui/dr-ui/src/histogram.rs:228`](../ui/dr-ui/src/histogram.rs#L228), [`ui/dr-ui/src/histogram.rs:272`](../ui/dr-ui/src/histogram.rs#L272), [`ui/dr-ui/src/histogram.rs:544`](../ui/dr-ui/src/histogram.rs#L544), [`ui/dr-ui/src/histogram.rs:565`](../ui/dr-ui/src/histogram.rs#L565), [`ui/dr-ui/src/histogram.rs:593`](../ui/dr-ui/src/histogram.rs#L593), [`ui/dr-ui/src/histogram.rs:621`](../ui/dr-ui/src/histogram.rs#L621), [`ui/dr-ui/src/histogram.rs:658`](../ui/dr-ui/src/histogram.rs#L658), [`ui/dr-ui/src/lib.rs:1637`](../ui/dr-ui/src/lib.rs#L1637), [`ui/dr-ui/src/lib.rs:1715`](../ui/dr-ui/src/lib.rs#L1715), [`ui/dr-ui/src/lib.rs:1814`](../ui/dr-ui/src/lib.rs#L1814), [`ui/dr-ui/src/lib.rs:1855`](../ui/dr-ui/src/lib.rs#L1855), [`ui/dr-ui/src/lib.rs:3125`](../ui/dr-ui/src/lib.rs#L3125), [`ui/dr-ui/src/lib.rs:383`](../ui/dr-ui/src/lib.rs#L383), [`ui/dr-ui/src/peaking.rs:1`](../ui/dr-ui/src/peaking.rs#L1), [`ui/dr-ui/ui/app.slint:1883`](../ui/dr-ui/ui/app.slint#L1883), [`ui/dr-ui/ui/app.slint:2502`](../ui/dr-ui/ui/app.slint#L2502), [`ui/dr-ui/ui/app.slint:95`](../ui/dr-ui/ui/app.slint#L95), [`ui/dr-ui/ui/peaking.slint:1`](../ui/dr-ui/ui/peaking.slint#L1), [`ui/dr-ui/ui/peaking.slint:25`](../ui/dr-ui/ui/peaking.slint#L25), [`ui/dr-ui/ui/peaking.slint:56`](../ui/dr-ui/ui/peaking.slint#L56) | +| FR-CULL-3 | [`core/dr-gpu/src/focus.rs:154`](../core/dr-gpu/src/focus.rs#L154), [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/raw_histogram.rs:129`](../core/dr-gpu/src/raw_histogram.rs#L129), [`core/dr-gpu/src/raw_histogram.rs:1`](../core/dr-gpu/src/raw_histogram.rs#L1), [`core/dr-gpu/src/raw_histogram.rs:272`](../core/dr-gpu/src/raw_histogram.rs#L272), [`core/dr-gpu/src/raw_histogram.rs:407`](../core/dr-gpu/src/raw_histogram.rs#L407), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`core/dr-gpu/src/shaders/raw_histogram.wgsl:1`](../core/dr-gpu/src/shaders/raw_histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:3411`](../ui/dr-ui/src/develop.rs#L3411), [`ui/dr-ui/src/develop.rs:3424`](../ui/dr-ui/src/develop.rs#L3424), [`ui/dr-ui/src/develop.rs:3469`](../ui/dr-ui/src/develop.rs#L3469), [`ui/dr-ui/src/develop.rs:3480`](../ui/dr-ui/src/develop.rs#L3480), [`ui/dr-ui/src/develop.rs:3486`](../ui/dr-ui/src/develop.rs#L3486), [`ui/dr-ui/src/develop.rs:3503`](../ui/dr-ui/src/develop.rs#L3503), [`ui/dr-ui/src/develop.rs:7069`](../ui/dr-ui/src/develop.rs#L7069), [`ui/dr-ui/src/develop.rs:7133`](../ui/dr-ui/src/develop.rs#L7133), [`ui/dr-ui/src/develop.rs:7156`](../ui/dr-ui/src/develop.rs#L7156), [`ui/dr-ui/src/develop.rs:763`](../ui/dr-ui/src/develop.rs#L763), [`ui/dr-ui/src/develop.rs:783`](../ui/dr-ui/src/develop.rs#L783), [`ui/dr-ui/src/develop.rs:789`](../ui/dr-ui/src/develop.rs#L789), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/histogram.rs:208`](../ui/dr-ui/src/histogram.rs#L208), [`ui/dr-ui/src/histogram.rs:228`](../ui/dr-ui/src/histogram.rs#L228), [`ui/dr-ui/src/histogram.rs:272`](../ui/dr-ui/src/histogram.rs#L272), [`ui/dr-ui/src/histogram.rs:544`](../ui/dr-ui/src/histogram.rs#L544), [`ui/dr-ui/src/histogram.rs:565`](../ui/dr-ui/src/histogram.rs#L565), [`ui/dr-ui/src/histogram.rs:593`](../ui/dr-ui/src/histogram.rs#L593), [`ui/dr-ui/src/histogram.rs:621`](../ui/dr-ui/src/histogram.rs#L621), [`ui/dr-ui/src/histogram.rs:658`](../ui/dr-ui/src/histogram.rs#L658), [`ui/dr-ui/src/lib.rs:1637`](../ui/dr-ui/src/lib.rs#L1637), [`ui/dr-ui/src/lib.rs:1715`](../ui/dr-ui/src/lib.rs#L1715), [`ui/dr-ui/src/lib.rs:1814`](../ui/dr-ui/src/lib.rs#L1814), [`ui/dr-ui/src/lib.rs:1855`](../ui/dr-ui/src/lib.rs#L1855), [`ui/dr-ui/src/lib.rs:3125`](../ui/dr-ui/src/lib.rs#L3125), [`ui/dr-ui/src/lib.rs:383`](../ui/dr-ui/src/lib.rs#L383), [`ui/dr-ui/src/peaking.rs:1`](../ui/dr-ui/src/peaking.rs#L1), [`ui/dr-ui/ui/app.slint:1886`](../ui/dr-ui/ui/app.slint#L1886), [`ui/dr-ui/ui/app.slint:2505`](../ui/dr-ui/ui/app.slint#L2505), [`ui/dr-ui/ui/app.slint:95`](../ui/dr-ui/ui/app.slint#L95), [`ui/dr-ui/ui/peaking.slint:1`](../ui/dr-ui/ui/peaking.slint#L1), [`ui/dr-ui/ui/peaking.slint:25`](../ui/dr-ui/ui/peaking.slint#L25), [`ui/dr-ui/ui/peaking.slint:56`](../ui/dr-ui/ui/peaking.slint#L56) | | FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:137`](../core/dr-pipeline/src/sidecar.rs#L137), [`ui/dr-ui/src/library.rs:254`](../ui/dr-ui/src/library.rs#L254), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500) | | FR-CULL-5 | [`core/dr-catalog/src/bursts.rs:1`](../core/dr-catalog/src/bursts.rs#L1), [`core/dr-catalog/src/schema.rs:438`](../core/dr-catalog/src/schema.rs#L438), [`ui/dr-ui/src/bursts.rs:1`](../ui/dr-ui/src/bursts.rs#L1), [`ui/dr-ui/src/library.rs:210`](../ui/dr-ui/src/library.rs#L210), [`ui/dr-ui/src/library.rs:5853`](../ui/dr-ui/src/library.rs#L5853) | | FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:506`](../core/dr-catalog/src/schema.rs#L506), [`core/dr-catalog/src/schema.rs:573`](../core/dr-catalog/src/schema.rs#L573), [`core/dr-catalog/src/schema.rs:614`](../core/dr-catalog/src/schema.rs#L614), [`core/dr-face/src/align.rs:291`](../core/dr-face/src/align.rs#L291), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/faces.rs:330`](../ui/dr-ui/src/faces.rs#L330), [`ui/dr-ui/src/faces.rs:346`](../ui/dr-ui/src/faces.rs#L346), [`ui/dr-ui/src/identity_ui.rs:540`](../ui/dr-ui/src/identity_ui.rs#L540), [`ui/dr-ui/src/lib.rs:1256`](../ui/dr-ui/src/lib.rs#L1256), [`ui/dr-ui/src/library.rs:3293`](../ui/dr-ui/src/library.rs#L3293), [`ui/dr-ui/src/library.rs:3385`](../ui/dr-ui/src/library.rs#L3385), [`ui/dr-ui/src/library.rs:3606`](../ui/dr-ui/src/library.rs#L3606), [`ui/dr-ui/src/library.rs:3742`](../ui/dr-ui/src/library.rs#L3742), [`ui/dr-ui/src/library.rs:3778`](../ui/dr-ui/src/library.rs#L3778), [`ui/dr-ui/ui/settings.slint:427`](../ui/dr-ui/ui/settings.slint#L427), [`ui/dr-ui/ui/settings.slint:82`](../ui/dr-ui/ui/settings.slint#L82) | @@ -60,35 +60,36 @@ _None._ | 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-12 | [`core/dr-pipeline/ops/colour_grading.yaml:1`](../core/dr-pipeline/ops/colour_grading.yaml#L1) | | 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-10 | [`core/dr-gpu/src/mask.rs:1062`](../core/dr-gpu/src/mask.rs#L1062), [`core/dr-gpu/src/mask.rs:1150`](../core/dr-gpu/src/mask.rs#L1150), [`core/dr-gpu/src/mask.rs:402`](../core/dr-gpu/src/mask.rs#L402), [`core/dr-gpu/src/mask.rs:444`](../core/dr-gpu/src/mask.rs#L444), [`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:658`](../core/dr-gpu/src/mask.rs#L658), [`core/dr-gpu/src/mask.rs:717`](../core/dr-gpu/src/mask.rs#L717), [`core/dr-gpu/src/mask.rs:829`](../core/dr-gpu/src/mask.rs#L829), [`core/dr-gpu/src/mask.rs:85`](../core/dr-gpu/src/mask.rs#L85), [`core/dr-gpu/src/shaders/mask.wgsl:101`](../core/dr-gpu/src/shaders/mask.wgsl#L101), [`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:2162`](../core/dr-pipeline/src/mask.rs#L2162), [`core/dr-pipeline/src/mask.rs:2175`](../core/dr-pipeline/src/mask.rs#L2175), [`core/dr-pipeline/src/mask.rs:2194`](../core/dr-pipeline/src/mask.rs#L2194), [`core/dr-pipeline/src/mask.rs:2208`](../core/dr-pipeline/src/mask.rs#L2208), [`core/dr-pipeline/src/mask.rs:2218`](../core/dr-pipeline/src/mask.rs#L2218), [`core/dr-pipeline/src/mask.rs:709`](../core/dr-pipeline/src/mask.rs#L709), [`core/dr-pipeline/src/mask.rs:748`](../core/dr-pipeline/src/mask.rs#L748), [`core/dr-pipeline/src/mask.rs:808`](../core/dr-pipeline/src/mask.rs#L808), [`core/dr-pipeline/src/mask.rs:827`](../core/dr-pipeline/src/mask.rs#L827), [`core/dr-pipeline/src/mask.rs:851`](../core/dr-pipeline/src/mask.rs#L851), [`core/dr-pipeline/src/mask.rs:863`](../core/dr-pipeline/src/mask.rs#L863), [`core/dr-pipeline/src/mask.rs:875`](../core/dr-pipeline/src/mask.rs#L875), [`core/dr-pipeline/src/sidecar.rs:1132`](../core/dr-pipeline/src/sidecar.rs#L1132), [`core/dr-pipeline/src/sidecar.rs:1320`](../core/dr-pipeline/src/sidecar.rs#L1320), [`core/dr-pipeline/src/sidecar.rs:1414`](../core/dr-pipeline/src/sidecar.rs#L1414), [`core/dr-pipeline/src/sidecar.rs:1518`](../core/dr-pipeline/src/sidecar.rs#L1518), [`core/dr-pipeline/src/sidecar.rs:1602`](../core/dr-pipeline/src/sidecar.rs#L1602), [`core/dr-pipeline/tests/mask_sidecar.rs:1132`](../core/dr-pipeline/tests/mask_sidecar.rs#L1132), [`core/dr-pipeline/tests/mask_sidecar.rs:1161`](../core/dr-pipeline/tests/mask_sidecar.rs#L1161), [`core/dr-pipeline/tests/mask_sidecar.rs:1179`](../core/dr-pipeline/tests/mask_sidecar.rs#L1179), [`core/dr-pipeline/tests/mask_sidecar.rs:1209`](../core/dr-pipeline/tests/mask_sidecar.rs#L1209), [`ui/dr-ui/src/develop.rs:2081`](../ui/dr-ui/src/develop.rs#L2081), [`ui/dr-ui/src/develop.rs:2936`](../ui/dr-ui/src/develop.rs#L2936), [`ui/dr-ui/src/develop.rs:2962`](../ui/dr-ui/src/develop.rs#L2962), [`ui/dr-ui/src/develop.rs:2994`](../ui/dr-ui/src/develop.rs#L2994), [`ui/dr-ui/src/develop.rs:3018`](../ui/dr-ui/src/develop.rs#L3018), [`ui/dr-ui/src/develop.rs:3027`](../ui/dr-ui/src/develop.rs#L3027), [`ui/dr-ui/src/develop.rs:3036`](../ui/dr-ui/src/develop.rs#L3036), [`ui/dr-ui/src/develop.rs:3056`](../ui/dr-ui/src/develop.rs#L3056), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/masks_ui.rs:628`](../ui/dr-ui/src/masks_ui.rs#L628), [`ui/dr-ui/src/masks_ui.rs:741`](../ui/dr-ui/src/masks_ui.rs#L741) | | 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:2217`](../core/dr-gpu/src/adjust.rs#L2217), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`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:209`](../core/dr-pipeline/src/graph.rs#L209), [`core/dr-pipeline/src/graph.rs:290`](../core/dr-pipeline/src/graph.rs#L290), [`core/dr-pipeline/src/graph.rs:735`](../core/dr-pipeline/src/graph.rs#L735), [`core/dr-pipeline/src/mask.rs:151`](../core/dr-pipeline/src/mask.rs#L151), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:535`](../core/dr-pipeline/src/operation.rs#L535), [`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/sidecar.rs:1165`](../core/dr-pipeline/src/sidecar.rs#L1165), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/src/sidecar.rs:1831`](../core/dr-pipeline/src/sidecar.rs#L1831), [`core/dr-pipeline/src/sidecar.rs:1891`](../core/dr-pipeline/src/sidecar.rs#L1891), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`core/dr-segment/src/refine.rs:850`](../core/dr-segment/src/refine.rs#L850), [`ui/dr-ui/src/develop.rs:103`](../ui/dr-ui/src/develop.rs#L103), [`ui/dr-ui/src/develop.rs:1517`](../ui/dr-ui/src/develop.rs#L1517), [`ui/dr-ui/src/develop.rs:165`](../ui/dr-ui/src/develop.rs#L165), [`ui/dr-ui/src/develop.rs:1933`](../ui/dr-ui/src/develop.rs#L1933), [`ui/dr-ui/src/develop.rs:2147`](../ui/dr-ui/src/develop.rs#L2147), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/develop.rs:2179`](../ui/dr-ui/src/develop.rs#L2179), [`ui/dr-ui/src/develop.rs:2201`](../ui/dr-ui/src/develop.rs#L2201), [`ui/dr-ui/src/develop.rs:2347`](../ui/dr-ui/src/develop.rs#L2347), [`ui/dr-ui/src/develop.rs:2445`](../ui/dr-ui/src/develop.rs#L2445), [`ui/dr-ui/src/develop.rs:328`](../ui/dr-ui/src/develop.rs#L328), [`ui/dr-ui/src/develop.rs:3502`](../ui/dr-ui/src/develop.rs#L3502), [`ui/dr-ui/src/develop.rs:3562`](../ui/dr-ui/src/develop.rs#L3562), [`ui/dr-ui/src/develop.rs:365`](../ui/dr-ui/src/develop.rs#L365), [`ui/dr-ui/src/develop.rs:3963`](../ui/dr-ui/src/develop.rs#L3963), [`ui/dr-ui/src/develop.rs:3993`](../ui/dr-ui/src/develop.rs#L3993), [`ui/dr-ui/src/develop.rs:4059`](../ui/dr-ui/src/develop.rs#L4059), [`ui/dr-ui/src/develop.rs:4073`](../ui/dr-ui/src/develop.rs#L4073), [`ui/dr-ui/src/develop.rs:4265`](../ui/dr-ui/src/develop.rs#L4265), [`ui/dr-ui/src/develop.rs:5093`](../ui/dr-ui/src/develop.rs#L5093), [`ui/dr-ui/src/develop.rs:5214`](../ui/dr-ui/src/develop.rs#L5214), [`ui/dr-ui/src/develop.rs:5370`](../ui/dr-ui/src/develop.rs#L5370), [`ui/dr-ui/src/develop.rs:5506`](../ui/dr-ui/src/develop.rs#L5506), [`ui/dr-ui/src/develop.rs:5550`](../ui/dr-ui/src/develop.rs#L5550), [`ui/dr-ui/src/develop.rs:5600`](../ui/dr-ui/src/develop.rs#L5600), [`ui/dr-ui/src/develop.rs:588`](../ui/dr-ui/src/develop.rs#L588), [`ui/dr-ui/src/develop.rs:649`](../ui/dr-ui/src/develop.rs#L649), [`ui/dr-ui/src/develop.rs:803`](../ui/dr-ui/src/develop.rs#L803), [`ui/dr-ui/src/develop.rs:850`](../ui/dr-ui/src/develop.rs#L850), [`ui/dr-ui/src/develop.rs:879`](../ui/dr-ui/src/develop.rs#L879), [`ui/dr-ui/src/lib.rs:1696`](../ui/dr-ui/src/lib.rs#L1696), [`ui/dr-ui/src/lib.rs:2518`](../ui/dr-ui/src/lib.rs#L2518), [`ui/dr-ui/src/lib.rs:2714`](../ui/dr-ui/src/lib.rs#L2714), [`ui/dr-ui/src/lib.rs:2886`](../ui/dr-ui/src/lib.rs#L2886), [`ui/dr-ui/src/lib.rs:2950`](../ui/dr-ui/src/lib.rs#L2950), [`ui/dr-ui/src/lib.rs:3004`](../ui/dr-ui/src/lib.rs#L3004), [`ui/dr-ui/src/lib.rs:389`](../ui/dr-ui/src/lib.rs#L389), [`ui/dr-ui/src/lib.rs:432`](../ui/dr-ui/src/lib.rs#L432), [`ui/dr-ui/src/lib.rs:455`](../ui/dr-ui/src/lib.rs#L455), [`ui/dr-ui/src/library.rs:547`](../ui/dr-ui/src/library.rs#L547), [`ui/dr-ui/src/masks_ui.rs:249`](../ui/dr-ui/src/masks_ui.rs#L249), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:884`](../ui/dr-ui/src/masks_ui.rs#L884), [`ui/dr-ui/src/masks_ui.rs:998`](../ui/dr-ui/src/masks_ui.rs#L998), [`ui/dr-ui/src/presets.rs:409`](../ui/dr-ui/src/presets.rs#L409), [`ui/dr-ui/src/segmentation.rs:116`](../ui/dr-ui/src/segmentation.rs#L116), [`ui/dr-ui/src/segmentation.rs:330`](../ui/dr-ui/src/segmentation.rs#L330), [`ui/dr-ui/src/segmentation.rs:346`](../ui/dr-ui/src/segmentation.rs#L346), [`ui/dr-ui/src/segmentation.rs:476`](../ui/dr-ui/src/segmentation.rs#L476), [`ui/dr-ui/src/segmentation.rs:504`](../ui/dr-ui/src/segmentation.rs#L504), [`ui/dr-ui/ui/adjust.slint:547`](../ui/dr-ui/ui/adjust.slint#L547), [`ui/dr-ui/ui/adjust.slint:666`](../ui/dr-ui/ui/adjust.slint#L666), [`ui/dr-ui/ui/app.slint:1008`](../ui/dr-ui/ui/app.slint#L1008), [`ui/dr-ui/ui/app.slint:156`](../ui/dr-ui/ui/app.slint#L156), [`ui/dr-ui/ui/app.slint:202`](../ui/dr-ui/ui/app.slint#L202), [`ui/dr-ui/ui/app.slint:2085`](../ui/dr-ui/ui/app.slint#L2085), [`ui/dr-ui/ui/app.slint:68`](../ui/dr-ui/ui/app.slint#L68), [`ui/dr-ui/ui/develop.slint:220`](../ui/dr-ui/ui/develop.slint#L220), [`ui/dr-ui/ui/masks.slint:612`](../ui/dr-ui/ui/masks.slint#L612) | -| FR-DEV-3a | [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:194`](../core/dr-pipeline/src/descriptor.rs#L194), [`core/dr-pipeline/src/descriptor.rs:234`](../core/dr-pipeline/src/descriptor.rs#L234), [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/descriptor.rs:313`](../core/dr-pipeline/src/descriptor.rs#L313), [`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:337`](../core/dr-pipeline/src/graph.rs#L337), [`core/dr-pipeline/src/graph.rs:47`](../core/dr-pipeline/src/graph.rs#L47), [`core/dr-pipeline/src/graph.rs:60`](../core/dr-pipeline/src/graph.rs#L60), [`core/dr-pipeline/src/mask.rs:1108`](../core/dr-pipeline/src/mask.rs#L1108), [`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.rs:1414`](../ui/dr-ui/src/develop.rs#L1414), [`ui/dr-ui/src/lib.rs:749`](../ui/dr-ui/src/lib.rs#L749), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../ui/dr-ui/tests/ui_names_no_operation.rs#L1) | +| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2217`](../core/dr-gpu/src/adjust.rs#L2217), [`core/dr-gpu/src/adjust.rs:651`](../core/dr-gpu/src/adjust.rs#L651), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`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:209`](../core/dr-pipeline/src/graph.rs#L209), [`core/dr-pipeline/src/graph.rs:290`](../core/dr-pipeline/src/graph.rs#L290), [`core/dr-pipeline/src/graph.rs:735`](../core/dr-pipeline/src/graph.rs#L735), [`core/dr-pipeline/src/mask.rs:205`](../core/dr-pipeline/src/mask.rs#L205), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:535`](../core/dr-pipeline/src/operation.rs#L535), [`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/sidecar.rs:1205`](../core/dr-pipeline/src/sidecar.rs#L1205), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/src/sidecar.rs:1918`](../core/dr-pipeline/src/sidecar.rs#L1918), [`core/dr-pipeline/src/sidecar.rs:1978`](../core/dr-pipeline/src/sidecar.rs#L1978), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`core/dr-segment/src/refine.rs:850`](../core/dr-segment/src/refine.rs#L850), [`ui/dr-ui/src/develop.rs:103`](../ui/dr-ui/src/develop.rs#L103), [`ui/dr-ui/src/develop.rs:1517`](../ui/dr-ui/src/develop.rs#L1517), [`ui/dr-ui/src/develop.rs:165`](../ui/dr-ui/src/develop.rs#L165), [`ui/dr-ui/src/develop.rs:1933`](../ui/dr-ui/src/develop.rs#L1933), [`ui/dr-ui/src/develop.rs:2160`](../ui/dr-ui/src/develop.rs#L2160), [`ui/dr-ui/src/develop.rs:2178`](../ui/dr-ui/src/develop.rs#L2178), [`ui/dr-ui/src/develop.rs:2192`](../ui/dr-ui/src/develop.rs#L2192), [`ui/dr-ui/src/develop.rs:2214`](../ui/dr-ui/src/develop.rs#L2214), [`ui/dr-ui/src/develop.rs:2360`](../ui/dr-ui/src/develop.rs#L2360), [`ui/dr-ui/src/develop.rs:2458`](../ui/dr-ui/src/develop.rs#L2458), [`ui/dr-ui/src/develop.rs:328`](../ui/dr-ui/src/develop.rs#L328), [`ui/dr-ui/src/develop.rs:3652`](../ui/dr-ui/src/develop.rs#L3652), [`ui/dr-ui/src/develop.rs:365`](../ui/dr-ui/src/develop.rs#L365), [`ui/dr-ui/src/develop.rs:3712`](../ui/dr-ui/src/develop.rs#L3712), [`ui/dr-ui/src/develop.rs:4113`](../ui/dr-ui/src/develop.rs#L4113), [`ui/dr-ui/src/develop.rs:4143`](../ui/dr-ui/src/develop.rs#L4143), [`ui/dr-ui/src/develop.rs:4209`](../ui/dr-ui/src/develop.rs#L4209), [`ui/dr-ui/src/develop.rs:4223`](../ui/dr-ui/src/develop.rs#L4223), [`ui/dr-ui/src/develop.rs:4415`](../ui/dr-ui/src/develop.rs#L4415), [`ui/dr-ui/src/develop.rs:5243`](../ui/dr-ui/src/develop.rs#L5243), [`ui/dr-ui/src/develop.rs:5364`](../ui/dr-ui/src/develop.rs#L5364), [`ui/dr-ui/src/develop.rs:5520`](../ui/dr-ui/src/develop.rs#L5520), [`ui/dr-ui/src/develop.rs:5656`](../ui/dr-ui/src/develop.rs#L5656), [`ui/dr-ui/src/develop.rs:5700`](../ui/dr-ui/src/develop.rs#L5700), [`ui/dr-ui/src/develop.rs:5750`](../ui/dr-ui/src/develop.rs#L5750), [`ui/dr-ui/src/develop.rs:588`](../ui/dr-ui/src/develop.rs#L588), [`ui/dr-ui/src/develop.rs:649`](../ui/dr-ui/src/develop.rs#L649), [`ui/dr-ui/src/develop.rs:803`](../ui/dr-ui/src/develop.rs#L803), [`ui/dr-ui/src/develop.rs:850`](../ui/dr-ui/src/develop.rs#L850), [`ui/dr-ui/src/develop.rs:879`](../ui/dr-ui/src/develop.rs#L879), [`ui/dr-ui/src/lib.rs:1696`](../ui/dr-ui/src/lib.rs#L1696), [`ui/dr-ui/src/lib.rs:2518`](../ui/dr-ui/src/lib.rs#L2518), [`ui/dr-ui/src/lib.rs:2714`](../ui/dr-ui/src/lib.rs#L2714), [`ui/dr-ui/src/lib.rs:2886`](../ui/dr-ui/src/lib.rs#L2886), [`ui/dr-ui/src/lib.rs:2950`](../ui/dr-ui/src/lib.rs#L2950), [`ui/dr-ui/src/lib.rs:3004`](../ui/dr-ui/src/lib.rs#L3004), [`ui/dr-ui/src/lib.rs:389`](../ui/dr-ui/src/lib.rs#L389), [`ui/dr-ui/src/lib.rs:432`](../ui/dr-ui/src/lib.rs#L432), [`ui/dr-ui/src/lib.rs:455`](../ui/dr-ui/src/lib.rs#L455), [`ui/dr-ui/src/library.rs:547`](../ui/dr-ui/src/library.rs#L547), [`ui/dr-ui/src/masks_ui.rs:1065`](../ui/dr-ui/src/masks_ui.rs#L1065), [`ui/dr-ui/src/masks_ui.rs:263`](../ui/dr-ui/src/masks_ui.rs#L263), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:951`](../ui/dr-ui/src/masks_ui.rs#L951), [`ui/dr-ui/src/presets.rs:409`](../ui/dr-ui/src/presets.rs#L409), [`ui/dr-ui/src/segmentation.rs:116`](../ui/dr-ui/src/segmentation.rs#L116), [`ui/dr-ui/src/segmentation.rs:330`](../ui/dr-ui/src/segmentation.rs#L330), [`ui/dr-ui/src/segmentation.rs:346`](../ui/dr-ui/src/segmentation.rs#L346), [`ui/dr-ui/src/segmentation.rs:476`](../ui/dr-ui/src/segmentation.rs#L476), [`ui/dr-ui/src/segmentation.rs:504`](../ui/dr-ui/src/segmentation.rs#L504), [`ui/dr-ui/ui/adjust.slint:547`](../ui/dr-ui/ui/adjust.slint#L547), [`ui/dr-ui/ui/adjust.slint:666`](../ui/dr-ui/ui/adjust.slint#L666), [`ui/dr-ui/ui/app.slint:1008`](../ui/dr-ui/ui/app.slint#L1008), [`ui/dr-ui/ui/app.slint:156`](../ui/dr-ui/ui/app.slint#L156), [`ui/dr-ui/ui/app.slint:202`](../ui/dr-ui/ui/app.slint#L202), [`ui/dr-ui/ui/app.slint:2088`](../ui/dr-ui/ui/app.slint#L2088), [`ui/dr-ui/ui/app.slint:68`](../ui/dr-ui/ui/app.slint#L68), [`ui/dr-ui/ui/develop.slint:220`](../ui/dr-ui/ui/develop.slint#L220), [`ui/dr-ui/ui/masks.slint:757`](../ui/dr-ui/ui/masks.slint#L757) | +| FR-DEV-3a | [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:194`](../core/dr-pipeline/src/descriptor.rs#L194), [`core/dr-pipeline/src/descriptor.rs:234`](../core/dr-pipeline/src/descriptor.rs#L234), [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/descriptor.rs:313`](../core/dr-pipeline/src/descriptor.rs#L313), [`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:337`](../core/dr-pipeline/src/graph.rs#L337), [`core/dr-pipeline/src/graph.rs:47`](../core/dr-pipeline/src/graph.rs#L47), [`core/dr-pipeline/src/graph.rs:60`](../core/dr-pipeline/src/graph.rs#L60), [`core/dr-pipeline/src/mask.rs:1331`](../core/dr-pipeline/src/mask.rs#L1331), [`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.rs:1414`](../ui/dr-ui/src/develop.rs#L1414), [`ui/dr-ui/src/lib.rs:749`](../ui/dr-ui/src/lib.rs#L749), [`ui/dr-ui/tests/ui_names_no_operation.rs:1`](../ui/dr-ui/tests/ui_names_no_operation.rs#L1) | | FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`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) | -| FR-DEV-3c | [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:337`](../core/dr-pipeline/src/graph.rs#L337), [`core/dr-pipeline/src/graph.rs:47`](../core/dr-pipeline/src/graph.rs#L47), [`core/dr-pipeline/src/mask.rs:1108`](../core/dr-pipeline/src/mask.rs#L1108), [`ui/dr-ui/src/develop.rs:6179`](../ui/dr-ui/src/develop.rs#L6179) | +| FR-DEV-3c | [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:337`](../core/dr-pipeline/src/graph.rs#L337), [`core/dr-pipeline/src/graph.rs:47`](../core/dr-pipeline/src/graph.rs#L47), [`core/dr-pipeline/src/mask.rs:1331`](../core/dr-pipeline/src/mask.rs#L1331), [`ui/dr-ui/src/develop.rs:6329`](../ui/dr-ui/src/develop.rs#L6329) | | FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`core/dr-gpu/src/adjust.rs:1079`](../core/dr-gpu/src/adjust.rs#L1079), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/adjust.rs:84`](../core/dr-gpu/src/adjust.rs#L84), [`core/dr-gpu/src/adjust.rs:986`](../core/dr-gpu/src/adjust.rs#L986), [`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:774`](../core/dr-pipeline/src/graph.rs#L774), [`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:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`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:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:967`](../core/dr-gpu/src/adjust.rs#L967), [`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:1762`](../core/dr-pipeline/src/operation.rs#L1762), [`core/dr-pipeline/src/operation.rs:1843`](../core/dr-pipeline/src/operation.rs#L1843), [`core/dr-pipeline/src/operation.rs:1868`](../core/dr-pipeline/src/operation.rs#L1868), [`core/dr-pipeline/src/operation.rs:1883`](../core/dr-pipeline/src/operation.rs#L1883), [`core/dr-pipeline/src/operation.rs:1907`](../core/dr-pipeline/src/operation.rs#L1907), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/operation.rs:454`](../core/dr-pipeline/src/operation.rs#L454), [`core/dr-pipeline/src/operation.rs:464`](../core/dr-pipeline/src/operation.rs#L464), [`core/dr-pipeline/src/operation.rs:625`](../core/dr-pipeline/src/operation.rs#L625) | -| 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:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`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:153`](../core/dr-pipeline/src/graph.rs#L153), [`core/dr-pipeline/src/graph.rs:444`](../core/dr-pipeline/src/graph.rs#L444), [`core/dr-pipeline/src/operation.rs:1137`](../core/dr-pipeline/src/operation.rs#L1137), [`core/dr-pipeline/src/operation.rs:1165`](../core/dr-pipeline/src/operation.rs#L1165), [`core/dr-pipeline/src/operation.rs:1211`](../core/dr-pipeline/src/operation.rs#L1211), [`core/dr-pipeline/src/operation.rs:1762`](../core/dr-pipeline/src/operation.rs#L1762), [`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:113`](../core/dr-pipeline/src/sidecar.rs#L113), [`core/dr-pipeline/src/sidecar.rs:169`](../core/dr-pipeline/src/sidecar.rs#L169), [`core/dr-pipeline/src/sidecar.rs:2177`](../core/dr-pipeline/src/sidecar.rs#L2177), [`core/dr-pipeline/src/sidecar.rs:2253`](../core/dr-pipeline/src/sidecar.rs#L2253), [`core/dr-pipeline/src/sidecar.rs:535`](../core/dr-pipeline/src/sidecar.rs#L535), [`core/dr-pipeline/src/sidecar.rs:779`](../core/dr-pipeline/src/sidecar.rs#L779), [`core/dr-pipeline/src/sidecar.rs:911`](../core/dr-pipeline/src/sidecar.rs#L911), [`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.rs:3710`](../ui/dr-ui/src/develop.rs#L3710), [`ui/dr-ui/src/develop.rs:3727`](../ui/dr-ui/src/develop.rs#L3727), [`ui/dr-ui/src/develop.rs:3739`](../ui/dr-ui/src/develop.rs#L3739), [`ui/dr-ui/src/develop.rs:3777`](../ui/dr-ui/src/develop.rs#L3777), [`ui/dr-ui/src/develop.rs:3786`](../ui/dr-ui/src/develop.rs#L3786), [`ui/dr-ui/src/develop.rs:3889`](../ui/dr-ui/src/develop.rs#L3889), [`ui/dr-ui/src/develop.rs:4302`](../ui/dr-ui/src/develop.rs#L4302), [`ui/dr-ui/src/develop.rs:4317`](../ui/dr-ui/src/develop.rs#L4317), [`ui/dr-ui/src/lib.rs:2492`](../ui/dr-ui/src/lib.rs#L2492), [`ui/dr-ui/src/lib.rs:682`](../ui/dr-ui/src/lib.rs#L682), [`ui/dr-ui/src/lib.rs:740`](../ui/dr-ui/src/lib.rs#L740), [`ui/dr-ui/src/library.rs:538`](../ui/dr-ui/src/library.rs#L538), [`ui/dr-ui/src/library.rs:800`](../ui/dr-ui/src/library.rs#L800), [`ui/dr-ui/src/presets.rs:417`](../ui/dr-ui/src/presets.rs#L417), [`ui/dr-ui/ui/adjust.slint:1072`](../ui/dr-ui/ui/adjust.slint#L1072), [`ui/dr-ui/ui/adjust.slint:996`](../ui/dr-ui/ui/adjust.slint#L996), [`ui/dr-ui/ui/app.slint:2699`](../ui/dr-ui/ui/app.slint#L2699), [`ui/dr-ui/ui/app.slint:718`](../ui/dr-ui/ui/app.slint#L718) | -| FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:1067`](../core/dr-pipeline/src/framing.rs#L1067), [`core/dr-pipeline/src/framing.rs:328`](../core/dr-pipeline/src/framing.rs#L328), [`core/dr-pipeline/src/framing.rs:498`](../core/dr-pipeline/src/framing.rs#L498), [`core/dr-types/src/lib.rs:339`](../core/dr-types/src/lib.rs#L339), [`core/dr-types/src/lib.rs:447`](../core/dr-types/src/lib.rs#L447), [`core/dr-types/src/lib.rs:459`](../core/dr-types/src/lib.rs#L459), [`core/dr-types/src/lib.rs:475`](../core/dr-types/src/lib.rs#L475), [`ui/dr-ui/src/develop.rs:140`](../ui/dr-ui/src/develop.rs#L140), [`ui/dr-ui/src/develop.rs:2364`](../ui/dr-ui/src/develop.rs#L2364), [`ui/dr-ui/src/segmentation.rs:476`](../ui/dr-ui/src/segmentation.rs#L476) | +| 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:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`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:153`](../core/dr-pipeline/src/graph.rs#L153), [`core/dr-pipeline/src/graph.rs:444`](../core/dr-pipeline/src/graph.rs#L444), [`core/dr-pipeline/src/operation.rs:1137`](../core/dr-pipeline/src/operation.rs#L1137), [`core/dr-pipeline/src/operation.rs:1165`](../core/dr-pipeline/src/operation.rs#L1165), [`core/dr-pipeline/src/operation.rs:1211`](../core/dr-pipeline/src/operation.rs#L1211), [`core/dr-pipeline/src/operation.rs:1762`](../core/dr-pipeline/src/operation.rs#L1762), [`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:113`](../core/dr-pipeline/src/sidecar.rs#L113), [`core/dr-pipeline/src/sidecar.rs:169`](../core/dr-pipeline/src/sidecar.rs#L169), [`core/dr-pipeline/src/sidecar.rs:2264`](../core/dr-pipeline/src/sidecar.rs#L2264), [`core/dr-pipeline/src/sidecar.rs:2340`](../core/dr-pipeline/src/sidecar.rs#L2340), [`core/dr-pipeline/src/sidecar.rs:535`](../core/dr-pipeline/src/sidecar.rs#L535), [`core/dr-pipeline/src/sidecar.rs:779`](../core/dr-pipeline/src/sidecar.rs#L779), [`core/dr-pipeline/src/sidecar.rs:911`](../core/dr-pipeline/src/sidecar.rs#L911), [`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.rs:3860`](../ui/dr-ui/src/develop.rs#L3860), [`ui/dr-ui/src/develop.rs:3877`](../ui/dr-ui/src/develop.rs#L3877), [`ui/dr-ui/src/develop.rs:3889`](../ui/dr-ui/src/develop.rs#L3889), [`ui/dr-ui/src/develop.rs:3927`](../ui/dr-ui/src/develop.rs#L3927), [`ui/dr-ui/src/develop.rs:3936`](../ui/dr-ui/src/develop.rs#L3936), [`ui/dr-ui/src/develop.rs:4039`](../ui/dr-ui/src/develop.rs#L4039), [`ui/dr-ui/src/develop.rs:4452`](../ui/dr-ui/src/develop.rs#L4452), [`ui/dr-ui/src/develop.rs:4467`](../ui/dr-ui/src/develop.rs#L4467), [`ui/dr-ui/src/lib.rs:2492`](../ui/dr-ui/src/lib.rs#L2492), [`ui/dr-ui/src/lib.rs:682`](../ui/dr-ui/src/lib.rs#L682), [`ui/dr-ui/src/lib.rs:740`](../ui/dr-ui/src/lib.rs#L740), [`ui/dr-ui/src/library.rs:538`](../ui/dr-ui/src/library.rs#L538), [`ui/dr-ui/src/library.rs:800`](../ui/dr-ui/src/library.rs#L800), [`ui/dr-ui/src/presets.rs:417`](../ui/dr-ui/src/presets.rs#L417), [`ui/dr-ui/ui/adjust.slint:1072`](../ui/dr-ui/ui/adjust.slint#L1072), [`ui/dr-ui/ui/adjust.slint:996`](../ui/dr-ui/ui/adjust.slint#L996), [`ui/dr-ui/ui/app.slint:2709`](../ui/dr-ui/ui/app.slint#L2709), [`ui/dr-ui/ui/app.slint:718`](../ui/dr-ui/ui/app.slint#L718) | +| FR-DEV-3h | [`core/dr-decode/src/lib.rs:404`](../core/dr-decode/src/lib.rs#L404), [`core/dr-decode/src/preview.rs:29`](../core/dr-decode/src/preview.rs#L29), [`core/dr-pipeline/src/framing.rs:1067`](../core/dr-pipeline/src/framing.rs#L1067), [`core/dr-pipeline/src/framing.rs:328`](../core/dr-pipeline/src/framing.rs#L328), [`core/dr-pipeline/src/framing.rs:498`](../core/dr-pipeline/src/framing.rs#L498), [`core/dr-types/src/lib.rs:339`](../core/dr-types/src/lib.rs#L339), [`core/dr-types/src/lib.rs:447`](../core/dr-types/src/lib.rs#L447), [`core/dr-types/src/lib.rs:459`](../core/dr-types/src/lib.rs#L459), [`core/dr-types/src/lib.rs:475`](../core/dr-types/src/lib.rs#L475), [`ui/dr-ui/src/develop.rs:140`](../ui/dr-ui/src/develop.rs#L140), [`ui/dr-ui/src/develop.rs:2377`](../ui/dr-ui/src/develop.rs#L2377), [`ui/dr-ui/src/segmentation.rs:476`](../ui/dr-ui/src/segmentation.rs#L476) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:351`](../core/dr-gpu/src/lib.rs#L351), [`ui/dr-ui/ui/crop.slint:1`](../ui/dr-ui/ui/crop.slint#L1) | -| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:465`](../core/dr-pipeline/src/graph.rs#L465), [`core/dr-pipeline/src/graph.rs:513`](../core/dr-pipeline/src/graph.rs#L513), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:3739`](../ui/dr-ui/src/develop.rs#L3739), [`ui/dr-ui/src/develop.rs:4317`](../ui/dr-ui/src/develop.rs#L4317), [`ui/dr-ui/src/develop.rs:4347`](../ui/dr-ui/src/develop.rs#L4347), [`ui/dr-ui/src/develop.rs:4360`](../ui/dr-ui/src/develop.rs#L4360), [`ui/dr-ui/src/develop.rs:4372`](../ui/dr-ui/src/develop.rs#L4372), [`ui/dr-ui/src/develop.rs:4388`](../ui/dr-ui/src/develop.rs#L4388), [`ui/dr-ui/src/develop.rs:4420`](../ui/dr-ui/src/develop.rs#L4420), [`ui/dr-ui/src/develop.rs:4424`](../ui/dr-ui/src/develop.rs#L4424), [`ui/dr-ui/src/develop.rs:4443`](../ui/dr-ui/src/develop.rs#L4443), [`ui/dr-ui/src/develop.rs:4459`](../ui/dr-ui/src/develop.rs#L4459), [`ui/dr-ui/src/develop.rs:746`](../ui/dr-ui/src/develop.rs#L746), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:233`](../ui/dr-ui/src/labels.rs#L233), [`ui/dr-ui/src/lib.rs:1631`](../ui/dr-ui/src/lib.rs#L1631), [`ui/dr-ui/src/lib.rs:1674`](../ui/dr-ui/src/lib.rs#L1674), [`ui/dr-ui/src/lib.rs:1682`](../ui/dr-ui/src/lib.rs#L1682), [`ui/dr-ui/src/lib.rs:2688`](../ui/dr-ui/src/lib.rs#L2688), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | -| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-pipeline/src/preset.rs:381`](../core/dr-pipeline/src/preset.rs#L381), [`core/dr-pipeline/src/preset.rs:414`](../core/dr-pipeline/src/preset.rs#L414), [`core/dr-pipeline/src/starter.rs:1`](../core/dr-pipeline/src/starter.rs#L1), [`core/dr-preset-xmp/src/lib.rs:1`](../core/dr-preset-xmp/src/lib.rs#L1), [`core/dr-types/src/settings.rs:272`](../core/dr-types/src/settings.rs#L272), [`ui/dr-ui/src/develop.rs:4259`](../ui/dr-ui/src/develop.rs#L4259), [`ui/dr-ui/src/develop.rs:4280`](../ui/dr-ui/src/develop.rs#L4280), [`ui/dr-ui/src/lib.rs:1598`](../ui/dr-ui/src/lib.rs#L1598), [`ui/dr-ui/src/lib.rs:2297`](../ui/dr-ui/src/lib.rs#L2297), [`ui/dr-ui/src/lib.rs:2326`](../ui/dr-ui/src/lib.rs#L2326), [`ui/dr-ui/src/library.rs:2219`](../ui/dr-ui/src/library.rs#L2219), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500), [`ui/dr-ui/src/library.rs:528`](../ui/dr-ui/src/library.rs#L528), [`ui/dr-ui/src/library_ui.rs:3199`](../ui/dr-ui/src/library_ui.rs#L3199), [`ui/dr-ui/src/library_ui.rs:3599`](../ui/dr-ui/src/library_ui.rs#L3599), [`ui/dr-ui/src/library_ui.rs:554`](../ui/dr-ui/src/library_ui.rs#L554), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:679`](../ui/dr-ui/src/presets.rs#L679), [`ui/dr-ui/src/presets.rs:713`](../ui/dr-ui/src/presets.rs#L713), [`ui/dr-ui/ui/adjust.slint:740`](../ui/dr-ui/ui/adjust.slint#L740), [`ui/dr-ui/ui/adjust.slint:767`](../ui/dr-ui/ui/adjust.slint#L767), [`ui/dr-ui/ui/adjust.slint:801`](../ui/dr-ui/ui/adjust.slint#L801), [`ui/dr-ui/ui/app.slint:1627`](../ui/dr-ui/ui/app.slint#L1627), [`ui/dr-ui/ui/app.slint:2788`](../ui/dr-ui/ui/app.slint#L2788), [`ui/dr-ui/ui/app.slint:802`](../ui/dr-ui/ui/app.slint#L802), [`ui/dr-ui/ui/app.slint:813`](../ui/dr-ui/ui/app.slint#L813), [`ui/dr-ui/ui/library.slint:1564`](../ui/dr-ui/ui/library.slint#L1564), [`ui/dr-ui/ui/library.slint:1570`](../ui/dr-ui/ui/library.slint#L1570), [`ui/dr-ui/ui/library.slint:4117`](../ui/dr-ui/ui/library.slint#L4117), [`ui/dr-ui/ui/library.slint:4132`](../ui/dr-ui/ui/library.slint#L4132), [`ui/dr-ui/ui/presets.slint:105`](../ui/dr-ui/ui/presets.slint#L105), [`ui/dr-ui/ui/presets.slint:113`](../ui/dr-ui/ui/presets.slint#L113), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/presets.slint:276`](../ui/dr-ui/ui/presets.slint#L276), [`ui/dr-ui/ui/presets.slint:297`](../ui/dr-ui/ui/presets.slint#L297), [`ui/dr-ui/ui/presets.slint:5`](../ui/dr-ui/ui/presets.slint#L5), [`ui/dr-ui/ui/presets.slint:60`](../ui/dr-ui/ui/presets.slint#L60), [`ui/dr-ui/ui/settings.slint:101`](../ui/dr-ui/ui/settings.slint#L101), [`ui/dr-ui/ui/settings.slint:595`](../ui/dr-ui/ui/settings.slint#L595) | -| FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:4388`](../ui/dr-ui/src/develop.rs#L4388), [`ui/dr-ui/src/develop.rs:4420`](../ui/dr-ui/src/develop.rs#L4420), [`ui/dr-ui/src/lib.rs:1682`](../ui/dr-ui/src/lib.rs#L1682), [`ui/dr-ui/src/lib.rs:2688`](../ui/dr-ui/src/lib.rs#L2688), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | -| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:405`](../core/dr-gpu/src/detail.rs#L405), [`core/dr-gpu/src/detail.rs:623`](../core/dr-gpu/src/detail.rs#L623), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:410`](../core/dr-pipeline/src/detail.rs#L410), [`core/dr-pipeline/src/detail.rs:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:469`](../core/dr-pipeline/src/detail.rs#L469), [`core/dr-pipeline/src/detail.rs:555`](../core/dr-pipeline/src/detail.rs#L555), [`core/dr-pipeline/src/graph.rs:115`](../core/dr-pipeline/src/graph.rs#L115), [`core/dr-pipeline/src/graph.rs:231`](../core/dr-pipeline/src/graph.rs#L231), [`core/dr-pipeline/src/graph.rs:859`](../core/dr-pipeline/src/graph.rs#L859), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:579`](../core/dr-pipeline/src/operation.rs#L579), [`core/dr-pipeline/src/sidecar.rs:1011`](../core/dr-pipeline/src/sidecar.rs#L1011), [`core/dr-pipeline/src/sidecar.rs:185`](../core/dr-pipeline/src/sidecar.rs#L185), [`core/dr-pipeline/src/sidecar.rs:354`](../core/dr-pipeline/src/sidecar.rs#L354), [`core/dr-pipeline/src/sidecar.rs:791`](../core/dr-pipeline/src/sidecar.rs#L791), [`core/dr-pipeline/src/sidecar.rs:927`](../core/dr-pipeline/src/sidecar.rs#L927), [`core/dr-pipeline/src/sidecar.rs:981`](../core/dr-pipeline/src/sidecar.rs#L981), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2516`](../ui/dr-ui/src/develop.rs#L2516), [`ui/dr-ui/src/develop.rs:2554`](../ui/dr-ui/src/develop.rs#L2554), [`ui/dr-ui/src/develop.rs:2619`](../ui/dr-ui/src/develop.rs#L2619), [`ui/dr-ui/src/develop.rs:2702`](../ui/dr-ui/src/develop.rs#L2702), [`ui/dr-ui/src/develop.rs:2716`](../ui/dr-ui/src/develop.rs#L2716), [`ui/dr-ui/src/develop.rs:833`](../ui/dr-ui/src/develop.rs#L833), [`ui/dr-ui/src/labels.rs:53`](../ui/dr-ui/src/labels.rs#L53), [`ui/dr-ui/src/lib.rs:1703`](../ui/dr-ui/src/lib.rs#L1703), [`ui/dr-ui/src/lib.rs:2806`](../ui/dr-ui/src/lib.rs#L2806), [`ui/dr-ui/src/lib.rs:394`](../ui/dr-ui/src/lib.rs#L394), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:832`](../ui/dr-ui/ui/adjust.slint#L832), [`ui/dr-ui/ui/app.slint:146`](../ui/dr-ui/ui/app.slint#L146), [`ui/dr-ui/ui/app.slint:1988`](../ui/dr-ui/ui/app.slint#L1988), [`ui/dr-ui/ui/app.slint:2163`](../ui/dr-ui/ui/app.slint#L2163), [`ui/dr-ui/ui/app.slint:2600`](../ui/dr-ui/ui/app.slint#L2600), [`ui/dr-ui/ui/icons.slint:338`](../ui/dr-ui/ui/icons.slint#L338), [`ui/dr-ui/ui/spots.slint:172`](../ui/dr-ui/ui/spots.slint#L172), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) | -| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2140`](../core/dr-gpu/src/adjust.rs#L2140), [`core/dr-gpu/src/adjust.rs:2217`](../core/dr-gpu/src/adjust.rs#L2217), [`core/dr-gpu/src/adjust.rs:2302`](../core/dr-gpu/src/adjust.rs#L2302), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:172`](../core/dr-gpu/src/lib.rs#L172), [`core/dr-gpu/src/lib.rs:192`](../core/dr-gpu/src/lib.rs#L192), [`core/dr-gpu/src/lib.rs:70`](../core/dr-gpu/src/lib.rs#L70), [`core/dr-gpu/src/lib.rs:93`](../core/dr-gpu/src/lib.rs#L93), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:264`](../core/dr-gpu/tests/local_contrast.rs#L264), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/graph.rs:705`](../core/dr-pipeline/src/graph.rs#L705), [`core/dr-pipeline/src/graph.rs:735`](../core/dr-pipeline/src/graph.rs#L735), [`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:659`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L659), [`core/dr-pipeline/src/ops/dehaze.rs:1`](../core/dr-pipeline/src/ops/dehaze.rs#L1), [`core/dr-pipeline/src/ops/dehaze.rs:592`](../core/dr-pipeline/src/ops/dehaze.rs#L592), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:971`](../core/dr-pipeline/src/ops/local_contrast.rs#L971), [`core/dr-pipeline/src/ops/noise_reduction.rs:700`](../core/dr-pipeline/src/ops/noise_reduction.rs#L700), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:3150`](../ui/dr-ui/src/develop.rs#L3150), [`ui/dr-ui/src/develop.rs:4714`](../ui/dr-ui/src/develop.rs#L4714), [`ui/dr-ui/src/develop.rs:5724`](../ui/dr-ui/src/develop.rs#L5724), [`ui/dr-ui/src/develop.rs:5758`](../ui/dr-ui/src/develop.rs#L5758), [`ui/dr-ui/src/lib.rs:890`](../ui/dr-ui/src/lib.rs#L890), [`ui/dr-ui/src/lib.rs:93`](../ui/dr-ui/src/lib.rs#L93), [`ui/dr-ui/src/lib.rs:949`](../ui/dr-ui/src/lib.rs#L949) | +| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:465`](../core/dr-pipeline/src/graph.rs#L465), [`core/dr-pipeline/src/graph.rs:513`](../core/dr-pipeline/src/graph.rs#L513), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:3889`](../ui/dr-ui/src/develop.rs#L3889), [`ui/dr-ui/src/develop.rs:4467`](../ui/dr-ui/src/develop.rs#L4467), [`ui/dr-ui/src/develop.rs:4497`](../ui/dr-ui/src/develop.rs#L4497), [`ui/dr-ui/src/develop.rs:4510`](../ui/dr-ui/src/develop.rs#L4510), [`ui/dr-ui/src/develop.rs:4522`](../ui/dr-ui/src/develop.rs#L4522), [`ui/dr-ui/src/develop.rs:4538`](../ui/dr-ui/src/develop.rs#L4538), [`ui/dr-ui/src/develop.rs:4570`](../ui/dr-ui/src/develop.rs#L4570), [`ui/dr-ui/src/develop.rs:4574`](../ui/dr-ui/src/develop.rs#L4574), [`ui/dr-ui/src/develop.rs:4593`](../ui/dr-ui/src/develop.rs#L4593), [`ui/dr-ui/src/develop.rs:4609`](../ui/dr-ui/src/develop.rs#L4609), [`ui/dr-ui/src/develop.rs:746`](../ui/dr-ui/src/develop.rs#L746), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:235`](../ui/dr-ui/src/labels.rs#L235), [`ui/dr-ui/src/lib.rs:1631`](../ui/dr-ui/src/lib.rs#L1631), [`ui/dr-ui/src/lib.rs:1674`](../ui/dr-ui/src/lib.rs#L1674), [`ui/dr-ui/src/lib.rs:1682`](../ui/dr-ui/src/lib.rs#L1682), [`ui/dr-ui/src/lib.rs:2688`](../ui/dr-ui/src/lib.rs#L2688), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | +| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-pipeline/src/preset.rs:381`](../core/dr-pipeline/src/preset.rs#L381), [`core/dr-pipeline/src/preset.rs:414`](../core/dr-pipeline/src/preset.rs#L414), [`core/dr-pipeline/src/starter.rs:1`](../core/dr-pipeline/src/starter.rs#L1), [`core/dr-preset-xmp/src/lib.rs:1`](../core/dr-preset-xmp/src/lib.rs#L1), [`core/dr-types/src/settings.rs:272`](../core/dr-types/src/settings.rs#L272), [`ui/dr-ui/src/develop.rs:4409`](../ui/dr-ui/src/develop.rs#L4409), [`ui/dr-ui/src/develop.rs:4430`](../ui/dr-ui/src/develop.rs#L4430), [`ui/dr-ui/src/lib.rs:1598`](../ui/dr-ui/src/lib.rs#L1598), [`ui/dr-ui/src/lib.rs:2297`](../ui/dr-ui/src/lib.rs#L2297), [`ui/dr-ui/src/lib.rs:2326`](../ui/dr-ui/src/lib.rs#L2326), [`ui/dr-ui/src/library.rs:2219`](../ui/dr-ui/src/library.rs#L2219), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500), [`ui/dr-ui/src/library.rs:528`](../ui/dr-ui/src/library.rs#L528), [`ui/dr-ui/src/library_ui.rs:3199`](../ui/dr-ui/src/library_ui.rs#L3199), [`ui/dr-ui/src/library_ui.rs:3599`](../ui/dr-ui/src/library_ui.rs#L3599), [`ui/dr-ui/src/library_ui.rs:554`](../ui/dr-ui/src/library_ui.rs#L554), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/presets.rs:679`](../ui/dr-ui/src/presets.rs#L679), [`ui/dr-ui/src/presets.rs:713`](../ui/dr-ui/src/presets.rs#L713), [`ui/dr-ui/ui/adjust.slint:740`](../ui/dr-ui/ui/adjust.slint#L740), [`ui/dr-ui/ui/adjust.slint:767`](../ui/dr-ui/ui/adjust.slint#L767), [`ui/dr-ui/ui/adjust.slint:801`](../ui/dr-ui/ui/adjust.slint#L801), [`ui/dr-ui/ui/app.slint:1630`](../ui/dr-ui/ui/app.slint#L1630), [`ui/dr-ui/ui/app.slint:2798`](../ui/dr-ui/ui/app.slint#L2798), [`ui/dr-ui/ui/app.slint:802`](../ui/dr-ui/ui/app.slint#L802), [`ui/dr-ui/ui/app.slint:813`](../ui/dr-ui/ui/app.slint#L813), [`ui/dr-ui/ui/library.slint:1564`](../ui/dr-ui/ui/library.slint#L1564), [`ui/dr-ui/ui/library.slint:1570`](../ui/dr-ui/ui/library.slint#L1570), [`ui/dr-ui/ui/library.slint:4117`](../ui/dr-ui/ui/library.slint#L4117), [`ui/dr-ui/ui/library.slint:4132`](../ui/dr-ui/ui/library.slint#L4132), [`ui/dr-ui/ui/presets.slint:105`](../ui/dr-ui/ui/presets.slint#L105), [`ui/dr-ui/ui/presets.slint:113`](../ui/dr-ui/ui/presets.slint#L113), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/presets.slint:276`](../ui/dr-ui/ui/presets.slint#L276), [`ui/dr-ui/ui/presets.slint:297`](../ui/dr-ui/ui/presets.slint#L297), [`ui/dr-ui/ui/presets.slint:5`](../ui/dr-ui/ui/presets.slint#L5), [`ui/dr-ui/ui/presets.slint:60`](../ui/dr-ui/ui/presets.slint#L60), [`ui/dr-ui/ui/settings.slint:101`](../ui/dr-ui/ui/settings.slint#L101), [`ui/dr-ui/ui/settings.slint:595`](../ui/dr-ui/ui/settings.slint#L595) | +| FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:4538`](../ui/dr-ui/src/develop.rs#L4538), [`ui/dr-ui/src/develop.rs:4570`](../ui/dr-ui/src/develop.rs#L4570), [`ui/dr-ui/src/lib.rs:1682`](../ui/dr-ui/src/lib.rs#L1682), [`ui/dr-ui/src/lib.rs:2688`](../ui/dr-ui/src/lib.rs#L2688), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) | +| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:405`](../core/dr-gpu/src/detail.rs#L405), [`core/dr-gpu/src/detail.rs:623`](../core/dr-gpu/src/detail.rs#L623), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:410`](../core/dr-pipeline/src/detail.rs#L410), [`core/dr-pipeline/src/detail.rs:434`](../core/dr-pipeline/src/detail.rs#L434), [`core/dr-pipeline/src/detail.rs:469`](../core/dr-pipeline/src/detail.rs#L469), [`core/dr-pipeline/src/detail.rs:555`](../core/dr-pipeline/src/detail.rs#L555), [`core/dr-pipeline/src/graph.rs:115`](../core/dr-pipeline/src/graph.rs#L115), [`core/dr-pipeline/src/graph.rs:231`](../core/dr-pipeline/src/graph.rs#L231), [`core/dr-pipeline/src/graph.rs:859`](../core/dr-pipeline/src/graph.rs#L859), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:579`](../core/dr-pipeline/src/operation.rs#L579), [`core/dr-pipeline/src/sidecar.rs:1011`](../core/dr-pipeline/src/sidecar.rs#L1011), [`core/dr-pipeline/src/sidecar.rs:185`](../core/dr-pipeline/src/sidecar.rs#L185), [`core/dr-pipeline/src/sidecar.rs:354`](../core/dr-pipeline/src/sidecar.rs#L354), [`core/dr-pipeline/src/sidecar.rs:791`](../core/dr-pipeline/src/sidecar.rs#L791), [`core/dr-pipeline/src/sidecar.rs:927`](../core/dr-pipeline/src/sidecar.rs#L927), [`core/dr-pipeline/src/sidecar.rs:981`](../core/dr-pipeline/src/sidecar.rs#L981), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2529`](../ui/dr-ui/src/develop.rs#L2529), [`ui/dr-ui/src/develop.rs:2567`](../ui/dr-ui/src/develop.rs#L2567), [`ui/dr-ui/src/develop.rs:2632`](../ui/dr-ui/src/develop.rs#L2632), [`ui/dr-ui/src/develop.rs:2715`](../ui/dr-ui/src/develop.rs#L2715), [`ui/dr-ui/src/develop.rs:2729`](../ui/dr-ui/src/develop.rs#L2729), [`ui/dr-ui/src/develop.rs:833`](../ui/dr-ui/src/develop.rs#L833), [`ui/dr-ui/src/labels.rs:56`](../ui/dr-ui/src/labels.rs#L56), [`ui/dr-ui/src/lib.rs:1703`](../ui/dr-ui/src/lib.rs#L1703), [`ui/dr-ui/src/lib.rs:2806`](../ui/dr-ui/src/lib.rs#L2806), [`ui/dr-ui/src/lib.rs:394`](../ui/dr-ui/src/lib.rs#L394), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/src/spots_ui.rs:1`](../ui/dr-ui/src/spots_ui.rs#L1), [`ui/dr-ui/src/spots_ui.rs:265`](../ui/dr-ui/src/spots_ui.rs#L265), [`ui/dr-ui/ui/adjust.slint:832`](../ui/dr-ui/ui/adjust.slint#L832), [`ui/dr-ui/ui/app.slint:146`](../ui/dr-ui/ui/app.slint#L146), [`ui/dr-ui/ui/app.slint:1991`](../ui/dr-ui/ui/app.slint#L1991), [`ui/dr-ui/ui/app.slint:2166`](../ui/dr-ui/ui/app.slint#L2166), [`ui/dr-ui/ui/app.slint:2603`](../ui/dr-ui/ui/app.slint#L2603), [`ui/dr-ui/ui/icons.slint:338`](../ui/dr-ui/ui/icons.slint#L338), [`ui/dr-ui/ui/spots.slint:172`](../ui/dr-ui/ui/spots.slint#L172), [`ui/dr-ui/ui/spots.slint:48`](../ui/dr-ui/ui/spots.slint#L48), [`ui/dr-ui/ui/spots.slint:5`](../ui/dr-ui/ui/spots.slint#L5) | +| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2140`](../core/dr-gpu/src/adjust.rs#L2140), [`core/dr-gpu/src/adjust.rs:2217`](../core/dr-gpu/src/adjust.rs#L2217), [`core/dr-gpu/src/adjust.rs:2302`](../core/dr-gpu/src/adjust.rs#L2302), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:172`](../core/dr-gpu/src/lib.rs#L172), [`core/dr-gpu/src/lib.rs:192`](../core/dr-gpu/src/lib.rs#L192), [`core/dr-gpu/src/lib.rs:70`](../core/dr-gpu/src/lib.rs#L70), [`core/dr-gpu/src/lib.rs:93`](../core/dr-gpu/src/lib.rs#L93), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:264`](../core/dr-gpu/tests/local_contrast.rs#L264), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:524`](../core/dr-pipeline/src/detail.rs#L524), [`core/dr-pipeline/src/graph.rs:705`](../core/dr-pipeline/src/graph.rs#L705), [`core/dr-pipeline/src/graph.rs:735`](../core/dr-pipeline/src/graph.rs#L735), [`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:659`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L659), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:971`](../core/dr-pipeline/src/ops/local_contrast.rs#L971), [`core/dr-pipeline/src/ops/noise_reduction.rs:700`](../core/dr-pipeline/src/ops/noise_reduction.rs#L700), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:3300`](../ui/dr-ui/src/develop.rs#L3300), [`ui/dr-ui/src/develop.rs:4864`](../ui/dr-ui/src/develop.rs#L4864), [`ui/dr-ui/src/develop.rs:5874`](../ui/dr-ui/src/develop.rs#L5874), [`ui/dr-ui/src/develop.rs:5908`](../ui/dr-ui/src/develop.rs#L5908), [`ui/dr-ui/src/lib.rs:890`](../ui/dr-ui/src/lib.rs#L890), [`ui/dr-ui/src/lib.rs:93`](../ui/dr-ui/src/lib.rs#L93), [`ui/dr-ui/src/lib.rs:949`](../ui/dr-ui/src/lib.rs#L949) | | FR-DSP-3 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101), [`core/dr-gpu/tests/local_contrast.rs:329`](../core/dr-gpu/tests/local_contrast.rs#L329), [`core/dr-pipeline/src/ops/local_contrast.rs:447`](../core/dr-pipeline/src/ops/local_contrast.rs#L447) | | FR-DSP-5 | [`core/dr-gpu/tests/frame_budget.rs:101`](../core/dr-gpu/tests/frame_budget.rs#L101), [`core/dr-gpu/tests/zoom_resolution.rs:135`](../core/dr-gpu/tests/zoom_resolution.rs#L135), [`core/dr-gpu/tests/zoom_resolution.rs:166`](../core/dr-gpu/tests/zoom_resolution.rs#L166), [`core/dr-gpu/tests/zoom_resolution.rs:1`](../core/dr-gpu/tests/zoom_resolution.rs#L1), [`core/dr-gpu/tests/zoom_resolution.rs:216`](../core/dr-gpu/tests/zoom_resolution.rs#L216) | -| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:497`](../core/dr-pipeline/src/operation.rs#L497), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`ui/dr-ui/src/develop.rs:3180`](../ui/dr-ui/src/develop.rs#L3180), [`ui/dr-ui/src/develop.rs:7152`](../ui/dr-ui/src/develop.rs#L7152), [`ui/dr-ui/src/lib.rs:1659`](../ui/dr-ui/src/lib.rs#L1659), [`ui/dr-ui/src/lib.rs:3189`](../ui/dr-ui/src/lib.rs#L3189) | -| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:3229`](../ui/dr-ui/src/develop.rs#L3229), [`ui/dr-ui/src/develop.rs:6826`](../ui/dr-ui/src/develop.rs#L6826), [`ui/dr-ui/src/develop.rs:6858`](../ui/dr-ui/src/develop.rs#L6858), [`ui/dr-ui/src/develop.rs:757`](../ui/dr-ui/src/develop.rs#L757), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1769`](../ui/dr-ui/src/lib.rs#L1769), [`ui/dr-ui/src/lib.rs:373`](../ui/dr-ui/src/lib.rs#L373), [`ui/dr-ui/ui/app.slint:80`](../ui/dr-ui/ui/app.slint#L80), [`ui/dr-ui/ui/histogram.slint:168`](../ui/dr-ui/ui/histogram.slint#L168), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | -| FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:3126`](../ui/dr-ui/src/develop.rs#L3126), [`ui/dr-ui/src/develop.rs:3180`](../ui/dr-ui/src/develop.rs#L3180), [`ui/dr-ui/src/develop.rs:7152`](../ui/dr-ui/src/develop.rs#L7152), [`ui/dr-ui/src/develop.rs:7198`](../ui/dr-ui/src/develop.rs#L7198), [`ui/dr-ui/src/develop.rs:7217`](../ui/dr-ui/src/develop.rs#L7217), [`ui/dr-ui/src/develop.rs:864`](../ui/dr-ui/src/develop.rs#L864), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1620`](../ui/dr-ui/src/lib.rs#L1620), [`ui/dr-ui/src/lib.rs:1659`](../ui/dr-ui/src/lib.rs#L1659), [`ui/dr-ui/src/lib.rs:3102`](../ui/dr-ui/src/lib.rs#L3102), [`ui/dr-ui/src/lib.rs:3189`](../ui/dr-ui/src/lib.rs#L3189), [`ui/dr-ui/ui/app.slint:1836`](../ui/dr-ui/ui/app.slint#L1836), [`ui/dr-ui/ui/app.slint:53`](../ui/dr-ui/ui/app.slint#L53), [`ui/dr-ui/ui/settings.slint:132`](../ui/dr-ui/ui/settings.slint#L132), [`ui/dr-ui/ui/settings.slint:832`](../ui/dr-ui/ui/settings.slint#L832) | +| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:497`](../core/dr-pipeline/src/operation.rs#L497), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`ui/dr-ui/src/develop.rs:3330`](../ui/dr-ui/src/develop.rs#L3330), [`ui/dr-ui/src/develop.rs:7302`](../ui/dr-ui/src/develop.rs#L7302), [`ui/dr-ui/src/lib.rs:1659`](../ui/dr-ui/src/lib.rs#L1659), [`ui/dr-ui/src/lib.rs:3189`](../ui/dr-ui/src/lib.rs#L3189) | +| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:3379`](../ui/dr-ui/src/develop.rs#L3379), [`ui/dr-ui/src/develop.rs:6976`](../ui/dr-ui/src/develop.rs#L6976), [`ui/dr-ui/src/develop.rs:7008`](../ui/dr-ui/src/develop.rs#L7008), [`ui/dr-ui/src/develop.rs:757`](../ui/dr-ui/src/develop.rs#L757), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1769`](../ui/dr-ui/src/lib.rs#L1769), [`ui/dr-ui/src/lib.rs:373`](../ui/dr-ui/src/lib.rs#L373), [`ui/dr-ui/ui/app.slint:80`](../ui/dr-ui/ui/app.slint#L80), [`ui/dr-ui/ui/histogram.slint:168`](../ui/dr-ui/ui/histogram.slint#L168), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | +| FR-DSP-8 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/icc.rs:1`](../platform/dr-plat/src/display/icc.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1), [`ui/dr-ui/src/develop.rs:3276`](../ui/dr-ui/src/develop.rs#L3276), [`ui/dr-ui/src/develop.rs:3330`](../ui/dr-ui/src/develop.rs#L3330), [`ui/dr-ui/src/develop.rs:7302`](../ui/dr-ui/src/develop.rs#L7302), [`ui/dr-ui/src/develop.rs:7348`](../ui/dr-ui/src/develop.rs#L7348), [`ui/dr-ui/src/develop.rs:7367`](../ui/dr-ui/src/develop.rs#L7367), [`ui/dr-ui/src/develop.rs:864`](../ui/dr-ui/src/develop.rs#L864), [`ui/dr-ui/src/display_ui.rs:192`](../ui/dr-ui/src/display_ui.rs#L192), [`ui/dr-ui/src/display_ui.rs:1`](../ui/dr-ui/src/display_ui.rs#L1), [`ui/dr-ui/src/display_ui.rs:325`](../ui/dr-ui/src/display_ui.rs#L325), [`ui/dr-ui/src/display_ui.rs:346`](../ui/dr-ui/src/display_ui.rs#L346), [`ui/dr-ui/src/display_ui.rs:379`](../ui/dr-ui/src/display_ui.rs#L379), [`ui/dr-ui/src/lib.rs:1620`](../ui/dr-ui/src/lib.rs#L1620), [`ui/dr-ui/src/lib.rs:1659`](../ui/dr-ui/src/lib.rs#L1659), [`ui/dr-ui/src/lib.rs:3102`](../ui/dr-ui/src/lib.rs#L3102), [`ui/dr-ui/src/lib.rs:3189`](../ui/dr-ui/src/lib.rs#L3189), [`ui/dr-ui/ui/app.slint:1839`](../ui/dr-ui/ui/app.slint#L1839), [`ui/dr-ui/ui/app.slint:53`](../ui/dr-ui/ui/app.slint#L53), [`ui/dr-ui/ui/settings.slint:132`](../ui/dr-ui/ui/settings.slint#L132), [`ui/dr-ui/ui/settings.slint:832`](../ui/dr-ui/ui/settings.slint#L832) | | FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | -| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2450`](../core/dr-gpu/src/adjust.rs#L2450), [`core/dr-pipeline/src/graph.rs:688`](../core/dr-pipeline/src/graph.rs#L688), [`core/dr-pipeline/src/graph.rs:749`](../core/dr-pipeline/src/graph.rs#L749), [`core/dr-pipeline/src/operation.rs:497`](../core/dr-pipeline/src/operation.rs#L497), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:761`](../core/dr-types/src/settings.rs#L761), [`ui/dr-ui/src/develop.rs:7217`](../ui/dr-ui/src/develop.rs#L7217), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | +| FR-EXP-2 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/error.rs:26`](../core/dr-export/src/error.rs#L26), [`core/dr-export/src/icc.rs:1`](../core/dr-export/src/icc.rs#L1), [`core/dr-export/src/lib.rs:153`](../core/dr-export/src/lib.rs#L153), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/lib.rs:53`](../core/dr-export/src/lib.rs#L53), [`core/dr-gpu/src/adjust.rs:2450`](../core/dr-gpu/src/adjust.rs#L2450), [`core/dr-pipeline/src/graph.rs:688`](../core/dr-pipeline/src/graph.rs#L688), [`core/dr-pipeline/src/graph.rs:749`](../core/dr-pipeline/src/graph.rs#L749), [`core/dr-pipeline/src/operation.rs:497`](../core/dr-pipeline/src/operation.rs#L497), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:761`](../core/dr-types/src/settings.rs#L761), [`ui/dr-ui/src/develop.rs:7367`](../ui/dr-ui/src/develop.rs#L7367), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-3 | [`core/dr-export/src/lib.rs:182`](../core/dr-export/src/lib.rs#L182), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/size.rs:136`](../core/dr-export/src/size.rs#L136), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`core/dr-export/src/size.rs:25`](../core/dr-export/src/size.rs#L25), [`core/dr-export/src/size.rs:84`](../core/dr-export/src/size.rs#L84), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`core/dr-types/src/settings.rs:806`](../core/dr-types/src/settings.rs#L806), [`core/dr-types/src/settings.rs:814`](../core/dr-types/src/settings.rs#L814), [`core/dr-types/src/settings.rs:830`](../core/dr-types/src/settings.rs#L830), [`core/dr-types/src/settings.rs:899`](../core/dr-types/src/settings.rs#L899), [`core/dr-types/src/settings.rs:927`](../core/dr-types/src/settings.rs#L927), [`core/dr-types/src/settings.rs:938`](../core/dr-types/src/settings.rs#L938), [`core/dr-types/src/settings.rs:944`](../core/dr-types/src/settings.rs#L944), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:237`](../ui/dr-ui/src/settings_ui.rs#L237), [`ui/dr-ui/src/settings_ui.rs:250`](../ui/dr-ui/src/settings_ui.rs#L250), [`ui/dr-ui/src/settings_ui.rs:265`](../ui/dr-ui/src/settings_ui.rs#L265), [`ui/dr-ui/src/settings_ui.rs:615`](../ui/dr-ui/src/settings_ui.rs#L615), [`ui/dr-ui/ui/settings.slint:735`](../ui/dr-ui/ui/settings.slint#L735), [`ui/dr-ui/ui/settings.slint:745`](../ui/dr-ui/ui/settings.slint#L745) | | FR-EXP-4 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/sharpen.rs:1`](../core/dr-export/src/sharpen.rs#L1), [`core/dr-export/src/size.rs:1`](../core/dr-export/src/size.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | | FR-EXP-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-EXP-6 | [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-export/src/name.rs:1`](../core/dr-export/src/name.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:484`](../ui/dr-ui/src/lib.rs#L484), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:49`](../ui/dr-ui/src/settings_ui.rs#L49), [`ui/dr-ui/src/settings_ui.rs:729`](../ui/dr-ui/src/settings_ui.rs#L729) | -| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:992`](../ui/dr-ui/src/export.rs#L992), [`ui/dr-ui/src/lib.rs:232`](../ui/dr-ui/src/lib.rs#L232), [`ui/dr-ui/src/lib.rs:2434`](../ui/dr-ui/src/lib.rs#L2434), [`ui/dr-ui/src/lib.rs:484`](../ui/dr-ui/src/lib.rs#L484), [`ui/dr-ui/src/lib.rs:530`](../ui/dr-ui/src/lib.rs#L530), [`ui/dr-ui/src/lib.rs:557`](../ui/dr-ui/src/lib.rs#L557), [`ui/dr-ui/src/library_ui.rs:3982`](../ui/dr-ui/src/library_ui.rs#L3982), [`ui/dr-ui/src/library_ui.rs:645`](../ui/dr-ui/src/library_ui.rs#L645), [`ui/dr-ui/src/library_ui.rs:745`](../ui/dr-ui/src/library_ui.rs#L745), [`ui/dr-ui/src/library_ui.rs:7673`](../ui/dr-ui/src/library_ui.rs#L7673), [`ui/dr-ui/src/library_ui.rs:7750`](../ui/dr-ui/src/library_ui.rs#L7750), [`ui/dr-ui/src/library_ui.rs:7762`](../ui/dr-ui/src/library_ui.rs#L7762), [`ui/dr-ui/src/library_ui.rs:802`](../ui/dr-ui/src/library_ui.rs#L802), [`ui/dr-ui/ui/app.slint:1147`](../ui/dr-ui/ui/app.slint#L1147), [`ui/dr-ui/ui/app.slint:1637`](../ui/dr-ui/ui/app.slint#L1637), [`ui/dr-ui/ui/library.slint:1573`](../ui/dr-ui/ui/library.slint#L1573), [`ui/dr-ui/ui/library.slint:4157`](../ui/dr-ui/ui/library.slint#L4157) | -| FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:655`](../core/dr-types/src/lib.rs#L655), [`core/dr-types/src/settings.rs:474`](../core/dr-types/src/settings.rs#L474), [`ui/dr-ui/src/develop.rs:3485`](../ui/dr-ui/src/develop.rs#L3485), [`ui/dr-ui/src/develop.rs:3593`](../ui/dr-ui/src/develop.rs#L3593), [`ui/dr-ui/src/develop.rs:703`](../ui/dr-ui/src/develop.rs#L703), [`ui/dr-ui/src/export.rs:467`](../ui/dr-ui/src/export.rs#L467), [`ui/dr-ui/src/export.rs:634`](../ui/dr-ui/src/export.rs#L634), [`ui/dr-ui/src/export.rs:680`](../ui/dr-ui/src/export.rs#L680), [`ui/dr-ui/src/export.rs:702`](../ui/dr-ui/src/export.rs#L702), [`ui/dr-ui/src/export.rs:826`](../ui/dr-ui/src/export.rs#L826), [`ui/dr-ui/src/export.rs:844`](../ui/dr-ui/src/export.rs#L844), [`ui/dr-ui/src/lib.rs:232`](../ui/dr-ui/src/lib.rs#L232), [`ui/dr-ui/src/lib.rs:255`](../ui/dr-ui/src/lib.rs#L255), [`ui/dr-ui/src/lib.rs:516`](../ui/dr-ui/src/lib.rs#L516), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | -| FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1106`](../core/dr-gpu/src/adjust.rs#L1106), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/develop.rs:3614`](../ui/dr-ui/src/develop.rs#L3614), [`ui/dr-ui/src/develop.rs:3652`](../ui/dr-ui/src/develop.rs#L3652), [`ui/dr-ui/src/develop.rs:5268`](../ui/dr-ui/src/develop.rs#L5268), [`ui/dr-ui/src/lib.rs:484`](../ui/dr-ui/src/lib.rs#L484), [`ui/dr-ui/src/library.rs:3385`](../ui/dr-ui/src/library.rs#L3385), [`ui/dr-ui/src/library.rs:3742`](../ui/dr-ui/src/library.rs#L3742), [`ui/dr-ui/src/library.rs:3778`](../ui/dr-ui/src/library.rs#L3778), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | +| FR-EXP-7 | [`ui/dr-ui/src/activity.rs:83`](../ui/dr-ui/src/activity.rs#L83), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/export.rs:992`](../ui/dr-ui/src/export.rs#L992), [`ui/dr-ui/src/lib.rs:232`](../ui/dr-ui/src/lib.rs#L232), [`ui/dr-ui/src/lib.rs:2434`](../ui/dr-ui/src/lib.rs#L2434), [`ui/dr-ui/src/lib.rs:484`](../ui/dr-ui/src/lib.rs#L484), [`ui/dr-ui/src/lib.rs:530`](../ui/dr-ui/src/lib.rs#L530), [`ui/dr-ui/src/lib.rs:557`](../ui/dr-ui/src/lib.rs#L557), [`ui/dr-ui/src/library_ui.rs:3982`](../ui/dr-ui/src/library_ui.rs#L3982), [`ui/dr-ui/src/library_ui.rs:645`](../ui/dr-ui/src/library_ui.rs#L645), [`ui/dr-ui/src/library_ui.rs:745`](../ui/dr-ui/src/library_ui.rs#L745), [`ui/dr-ui/src/library_ui.rs:7673`](../ui/dr-ui/src/library_ui.rs#L7673), [`ui/dr-ui/src/library_ui.rs:7750`](../ui/dr-ui/src/library_ui.rs#L7750), [`ui/dr-ui/src/library_ui.rs:7762`](../ui/dr-ui/src/library_ui.rs#L7762), [`ui/dr-ui/src/library_ui.rs:802`](../ui/dr-ui/src/library_ui.rs#L802), [`ui/dr-ui/ui/app.slint:1150`](../ui/dr-ui/ui/app.slint#L1150), [`ui/dr-ui/ui/app.slint:1640`](../ui/dr-ui/ui/app.slint#L1640), [`ui/dr-ui/ui/library.slint:1573`](../ui/dr-ui/ui/library.slint#L1573), [`ui/dr-ui/ui/library.slint:4157`](../ui/dr-ui/ui/library.slint#L4157) | +| FR-EXP-8 | [`core/dr-decode/src/lib.rs:326`](../core/dr-decode/src/lib.rs#L326), [`core/dr-decode/src/lib.rs:350`](../core/dr-decode/src/lib.rs#L350), [`core/dr-decode/src/lib.rs:364`](../core/dr-decode/src/lib.rs#L364), [`core/dr-decode/src/lib.rs:71`](../core/dr-decode/src/lib.rs#L71), [`core/dr-decode/src/lib.rs:79`](../core/dr-decode/src/lib.rs#L79), [`core/dr-decode/src/lib.rs:82`](../core/dr-decode/src/lib.rs#L82), [`core/dr-decode/src/locate.rs:1164`](../core/dr-decode/src/locate.rs#L1164), [`core/dr-decode/src/locate.rs:1223`](../core/dr-decode/src/locate.rs#L1223), [`core/dr-decode/src/locate.rs:316`](../core/dr-decode/src/locate.rs#L316), [`core/dr-decode/src/locate.rs:487`](../core/dr-decode/src/locate.rs#L487), [`core/dr-decode/src/locate.rs:571`](../core/dr-decode/src/locate.rs#L571), [`core/dr-decode/src/locate.rs:584`](../core/dr-decode/src/locate.rs#L584), [`core/dr-decode/src/locate.rs:667`](../core/dr-decode/src/locate.rs#L667), [`core/dr-export/examples/export.rs:99`](../core/dr-export/examples/export.rs#L99), [`core/dr-export/src/encode.rs:117`](../core/dr-export/src/encode.rs#L117), [`core/dr-export/src/encode.rs:161`](../core/dr-export/src/encode.rs#L161), [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/encode.rs:206`](../core/dr-export/src/encode.rs#L206), [`core/dr-export/src/encode.rs:235`](../core/dr-export/src/encode.rs#L235), [`core/dr-export/src/encode.rs:311`](../core/dr-export/src/encode.rs#L311), [`core/dr-export/src/encode.rs:325`](../core/dr-export/src/encode.rs#L325), [`core/dr-export/src/encode.rs:408`](../core/dr-export/src/encode.rs#L408), [`core/dr-export/src/encode.rs:456`](../core/dr-export/src/encode.rs#L456), [`core/dr-export/src/encode.rs:70`](../core/dr-export/src/encode.rs#L70), [`core/dr-export/src/encode.rs:795`](../core/dr-export/src/encode.rs#L795), [`core/dr-export/src/encode.rs:809`](../core/dr-export/src/encode.rs#L809), [`core/dr-export/src/encode.rs:850`](../core/dr-export/src/encode.rs#L850), [`core/dr-export/src/encode.rs:898`](../core/dr-export/src/encode.rs#L898), [`core/dr-export/src/exif.rs:1`](../core/dr-export/src/exif.rs#L1), [`core/dr-export/src/lib.rs:136`](../core/dr-export/src/lib.rs#L136), [`core/dr-export/src/metadata.rs:1`](../core/dr-export/src/metadata.rs#L1), [`core/dr-export/src/metadata.rs:41`](../core/dr-export/src/metadata.rs#L41), [`core/dr-export/src/metadata.rs:74`](../core/dr-export/src/metadata.rs#L74), [`core/dr-types/src/lib.rs:655`](../core/dr-types/src/lib.rs#L655), [`core/dr-types/src/settings.rs:474`](../core/dr-types/src/settings.rs#L474), [`ui/dr-ui/src/develop.rs:3635`](../ui/dr-ui/src/develop.rs#L3635), [`ui/dr-ui/src/develop.rs:3743`](../ui/dr-ui/src/develop.rs#L3743), [`ui/dr-ui/src/develop.rs:703`](../ui/dr-ui/src/develop.rs#L703), [`ui/dr-ui/src/export.rs:467`](../ui/dr-ui/src/export.rs#L467), [`ui/dr-ui/src/export.rs:634`](../ui/dr-ui/src/export.rs#L634), [`ui/dr-ui/src/export.rs:680`](../ui/dr-ui/src/export.rs#L680), [`ui/dr-ui/src/export.rs:702`](../ui/dr-ui/src/export.rs#L702), [`ui/dr-ui/src/export.rs:826`](../ui/dr-ui/src/export.rs#L826), [`ui/dr-ui/src/export.rs:844`](../ui/dr-ui/src/export.rs#L844), [`ui/dr-ui/src/lib.rs:232`](../ui/dr-ui/src/lib.rs#L232), [`ui/dr-ui/src/lib.rs:255`](../ui/dr-ui/src/lib.rs#L255), [`ui/dr-ui/src/lib.rs:516`](../ui/dr-ui/src/lib.rs#L516), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) | +| FR-EXP-9 | [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-export/src/lib.rs:128`](../core/dr-export/src/lib.rs#L128), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-gpu/src/adjust.rs:1106`](../core/dr-gpu/src/adjust.rs#L1106), [`ui/dr-ui/examples/face_native.rs:1`](../ui/dr-ui/examples/face_native.rs#L1), [`ui/dr-ui/src/develop.rs:3764`](../ui/dr-ui/src/develop.rs#L3764), [`ui/dr-ui/src/develop.rs:3802`](../ui/dr-ui/src/develop.rs#L3802), [`ui/dr-ui/src/develop.rs:5418`](../ui/dr-ui/src/develop.rs#L5418), [`ui/dr-ui/src/lib.rs:484`](../ui/dr-ui/src/lib.rs#L484), [`ui/dr-ui/src/library.rs:3385`](../ui/dr-ui/src/library.rs#L3385), [`ui/dr-ui/src/library.rs:3742`](../ui/dr-ui/src/library.rs#L3742), [`ui/dr-ui/src/library.rs:3778`](../ui/dr-ui/src/library.rs#L3778), [`ui/dr-ui/tests/export_ignores_the_viewport.rs:1`](../ui/dr-ui/tests/export_ignores_the_viewport.rs#L1) | | FR-NC-1 | [`core/dr-sync-nextcloud/src/auth.rs:132`](../core/dr-sync-nextcloud/src/auth.rs#L132), [`core/dr-sync-nextcloud/src/auth.rs:44`](../core/dr-sync-nextcloud/src/auth.rs#L44), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:355`](../core/dr-sync/src/account.rs#L355), [`ui/dr-ui/src/launch.rs:277`](../ui/dr-ui/src/launch.rs#L277), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61), [`ui/dr-ui/src/launch_ui.rs:417`](../ui/dr-ui/src/launch_ui.rs#L417) | | FR-NC-10 | [`core/dr-sync/src/account.rs:220`](../core/dr-sync/src/account.rs#L220), [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:557`](../ui/dr-ui/src/lib.rs#L557), [`ui/dr-ui/src/library.rs:1155`](../ui/dr-ui/src/library.rs#L1155), [`ui/dr-ui/src/library.rs:2255`](../ui/dr-ui/src/library.rs#L2255), [`ui/dr-ui/src/library.rs:584`](../ui/dr-ui/src/library.rs#L584), [`ui/dr-ui/src/library.rs:899`](../ui/dr-ui/src/library.rs#L899), [`ui/dr-ui/src/library_ui.rs:1983`](../ui/dr-ui/src/library_ui.rs#L1983), [`ui/dr-ui/src/library_ui.rs:3982`](../ui/dr-ui/src/library_ui.rs#L3982), [`ui/dr-ui/src/library_ui.rs:586`](../ui/dr-ui/src/library_ui.rs#L586), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-NC-12 | [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:40`](../core/dr-sync-nextcloud/src/lib.rs#L40), [`core/dr-sync-nextcloud/src/lib.rs:914`](../core/dr-sync-nextcloud/src/lib.rs#L914), [`core/dr-sync-nextcloud/src/provider.rs:1`](../core/dr-sync-nextcloud/src/provider.rs#L1), [`core/dr-sync/src/account.rs:1`](../core/dr-sync/src/account.rs#L1), [`core/dr-sync/src/account.rs:81`](../core/dr-sync/src/account.rs#L81), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/lib.rs:51`](../core/dr-sync/src/lib.rs#L51), [`core/dr-sync/src/provider.rs:106`](../core/dr-sync/src/provider.rs#L106), [`core/dr-sync/src/provider.rs:1`](../core/dr-sync/src/provider.rs#L1), [`core/dr-sync/src/provider.rs:53`](../core/dr-sync/src/provider.rs#L53), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`ui/dr-ui/src/remote.rs:1`](../ui/dr-ui/src/remote.rs#L1) | @@ -98,48 +99,48 @@ _None._ | FR-NC-4 | [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-nextcloud/src/propfind.rs:103`](../core/dr-sync-nextcloud/src/propfind.rs#L103), [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`core/dr-sync/src/capability.rs:6`](../core/dr-sync/src/capability.rs#L6), [`core/dr-sync/src/lib.rs:218`](../core/dr-sync/src/lib.rs#L218), [`core/dr-sync/src/scan.rs:118`](../core/dr-sync/src/scan.rs#L118), [`ui/dr-ui/src/launch.rs:61`](../ui/dr-ui/src/launch.rs#L61) | | FR-NC-5 | [`core/dr-sync-nextcloud/src/propfind.rs:51`](../core/dr-sync-nextcloud/src/propfind.rs#L51), [`ui/dr-ui/src/import.rs:488`](../ui/dr-ui/src/import.rs#L488) | | FR-NC-6 | [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1) | -| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1186`](../core/dr-catalog/src/schema.rs#L1186), [`core/dr-catalog/src/schema.rs:785`](../core/dr-catalog/src/schema.rs#L785), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3413`](../ui/dr-ui/src/collections_ui.rs#L3413), [`ui/dr-ui/src/collections_ui.rs:636`](../ui/dr-ui/src/collections_ui.rs#L636), [`ui/dr-ui/src/collections_ui.rs:704`](../ui/dr-ui/src/collections_ui.rs#L704), [`ui/dr-ui/src/lib.rs:2132`](../ui/dr-ui/src/lib.rs#L2132), [`ui/dr-ui/src/lib.rs:3261`](../ui/dr-ui/src/lib.rs#L3261), [`ui/dr-ui/src/library.rs:1881`](../ui/dr-ui/src/library.rs#L1881), [`ui/dr-ui/src/library.rs:1904`](../ui/dr-ui/src/library.rs#L1904), [`ui/dr-ui/src/library.rs:2179`](../ui/dr-ui/src/library.rs#L2179), [`ui/dr-ui/src/library_ui.rs:1408`](../ui/dr-ui/src/library_ui.rs#L1408), [`ui/dr-ui/src/library_ui.rs:1481`](../ui/dr-ui/src/library_ui.rs#L1481), [`ui/dr-ui/src/library_ui.rs:1582`](../ui/dr-ui/src/library_ui.rs#L1582), [`ui/dr-ui/src/library_ui.rs:1637`](../ui/dr-ui/src/library_ui.rs#L1637), [`ui/dr-ui/src/library_ui.rs:1772`](../ui/dr-ui/src/library_ui.rs#L1772), [`ui/dr-ui/src/library_ui.rs:1890`](../ui/dr-ui/src/library_ui.rs#L1890), [`ui/dr-ui/src/library_ui.rs:2657`](../ui/dr-ui/src/library_ui.rs#L2657), [`ui/dr-ui/src/library_ui.rs:341`](../ui/dr-ui/src/library_ui.rs#L341), [`ui/dr-ui/src/library_ui.rs:352`](../ui/dr-ui/src/library_ui.rs#L352), [`ui/dr-ui/src/library_ui.rs:360`](../ui/dr-ui/src/library_ui.rs#L360), [`ui/dr-ui/src/library_ui.rs:372`](../ui/dr-ui/src/library_ui.rs#L372), [`ui/dr-ui/src/library_ui.rs:381`](../ui/dr-ui/src/library_ui.rs#L381), [`ui/dr-ui/src/library_ui.rs:482`](../ui/dr-ui/src/library_ui.rs#L482), [`ui/dr-ui/src/library_ui.rs:492`](../ui/dr-ui/src/library_ui.rs#L492), [`ui/dr-ui/src/library_ui.rs:539`](../ui/dr-ui/src/library_ui.rs#L539), [`ui/dr-ui/src/library_ui.rs:602`](../ui/dr-ui/src/library_ui.rs#L602), [`ui/dr-ui/src/library_ui.rs:633`](../ui/dr-ui/src/library_ui.rs#L633), [`ui/dr-ui/src/library_ui.rs:645`](../ui/dr-ui/src/library_ui.rs#L645), [`ui/dr-ui/src/library_ui.rs:6536`](../ui/dr-ui/src/library_ui.rs#L6536), [`ui/dr-ui/src/library_ui.rs:6554`](../ui/dr-ui/src/library_ui.rs#L6554), [`ui/dr-ui/src/library_ui.rs:6566`](../ui/dr-ui/src/library_ui.rs#L6566), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:1535`](../ui/dr-ui/ui/app.slint#L1535), [`ui/dr-ui/ui/app.slint:2778`](../ui/dr-ui/ui/app.slint#L2778), [`ui/dr-ui/ui/app.slint:566`](../ui/dr-ui/ui/app.slint#L566), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:360`](../ui/dr-ui/ui/collections.slint#L360), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:372`](../ui/dr-ui/ui/collections.slint#L372), [`ui/dr-ui/ui/collections.slint:395`](../ui/dr-ui/ui/collections.slint#L395), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:596`](../ui/dr-ui/ui/collections.slint#L596), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:261`](../ui/dr-ui/ui/icons.slint#L261) | +| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-catalog/src/schema.rs:1186`](../core/dr-catalog/src/schema.rs#L1186), [`core/dr-catalog/src/schema.rs:785`](../core/dr-catalog/src/schema.rs#L785), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3413`](../ui/dr-ui/src/collections_ui.rs#L3413), [`ui/dr-ui/src/collections_ui.rs:636`](../ui/dr-ui/src/collections_ui.rs#L636), [`ui/dr-ui/src/collections_ui.rs:704`](../ui/dr-ui/src/collections_ui.rs#L704), [`ui/dr-ui/src/lib.rs:2132`](../ui/dr-ui/src/lib.rs#L2132), [`ui/dr-ui/src/lib.rs:3261`](../ui/dr-ui/src/lib.rs#L3261), [`ui/dr-ui/src/library.rs:1881`](../ui/dr-ui/src/library.rs#L1881), [`ui/dr-ui/src/library.rs:1904`](../ui/dr-ui/src/library.rs#L1904), [`ui/dr-ui/src/library.rs:2179`](../ui/dr-ui/src/library.rs#L2179), [`ui/dr-ui/src/library_ui.rs:1408`](../ui/dr-ui/src/library_ui.rs#L1408), [`ui/dr-ui/src/library_ui.rs:1481`](../ui/dr-ui/src/library_ui.rs#L1481), [`ui/dr-ui/src/library_ui.rs:1582`](../ui/dr-ui/src/library_ui.rs#L1582), [`ui/dr-ui/src/library_ui.rs:1637`](../ui/dr-ui/src/library_ui.rs#L1637), [`ui/dr-ui/src/library_ui.rs:1772`](../ui/dr-ui/src/library_ui.rs#L1772), [`ui/dr-ui/src/library_ui.rs:1890`](../ui/dr-ui/src/library_ui.rs#L1890), [`ui/dr-ui/src/library_ui.rs:2657`](../ui/dr-ui/src/library_ui.rs#L2657), [`ui/dr-ui/src/library_ui.rs:341`](../ui/dr-ui/src/library_ui.rs#L341), [`ui/dr-ui/src/library_ui.rs:352`](../ui/dr-ui/src/library_ui.rs#L352), [`ui/dr-ui/src/library_ui.rs:360`](../ui/dr-ui/src/library_ui.rs#L360), [`ui/dr-ui/src/library_ui.rs:372`](../ui/dr-ui/src/library_ui.rs#L372), [`ui/dr-ui/src/library_ui.rs:381`](../ui/dr-ui/src/library_ui.rs#L381), [`ui/dr-ui/src/library_ui.rs:482`](../ui/dr-ui/src/library_ui.rs#L482), [`ui/dr-ui/src/library_ui.rs:492`](../ui/dr-ui/src/library_ui.rs#L492), [`ui/dr-ui/src/library_ui.rs:539`](../ui/dr-ui/src/library_ui.rs#L539), [`ui/dr-ui/src/library_ui.rs:602`](../ui/dr-ui/src/library_ui.rs#L602), [`ui/dr-ui/src/library_ui.rs:633`](../ui/dr-ui/src/library_ui.rs#L633), [`ui/dr-ui/src/library_ui.rs:645`](../ui/dr-ui/src/library_ui.rs#L645), [`ui/dr-ui/src/library_ui.rs:6536`](../ui/dr-ui/src/library_ui.rs#L6536), [`ui/dr-ui/src/library_ui.rs:6554`](../ui/dr-ui/src/library_ui.rs#L6554), [`ui/dr-ui/src/library_ui.rs:6566`](../ui/dr-ui/src/library_ui.rs#L6566), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/ui/app.slint:1538`](../ui/dr-ui/ui/app.slint#L1538), [`ui/dr-ui/ui/app.slint:2788`](../ui/dr-ui/ui/app.slint#L2788), [`ui/dr-ui/ui/app.slint:566`](../ui/dr-ui/ui/app.slint#L566), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:360`](../ui/dr-ui/ui/collections.slint#L360), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:372`](../ui/dr-ui/ui/collections.slint#L372), [`ui/dr-ui/ui/collections.slint:395`](../ui/dr-ui/ui/collections.slint#L395), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:596`](../ui/dr-ui/ui/collections.slint#L596), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:261`](../ui/dr-ui/ui/icons.slint#L261) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1637`](../ui/dr-ui/src/library_ui.rs#L1637), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | | FR-NC-6c | [`core/dr-catalog/src/cache.rs:225`](../core/dr-catalog/src/cache.rs#L225), [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:197`](../core/dr-sync-folder/src/lib.rs#L197), [`core/dr-sync-folder/src/lib.rs:73`](../core/dr-sync-folder/src/lib.rs#L73), [`core/dr-sync-folder/src/lib.rs:818`](../core/dr-sync-folder/src/lib.rs#L818), [`core/dr-sync-folder/src/lib.rs:857`](../core/dr-sync-folder/src/lib.rs#L857), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1), [`core/dr-sync-nextcloud/src/desktop_client.rs:167`](../core/dr-sync-nextcloud/src/desktop_client.rs#L167), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-sync/src/error.rs:39`](../core/dr-sync/src/error.rs#L39), [`core/dr-sync/src/lib.rs:158`](../core/dr-sync/src/lib.rs#L158), [`core/dr-sync/src/types.rs:101`](../core/dr-sync/src/types.rs#L101), [`core/dr-types/src/lib.rs:122`](../core/dr-types/src/lib.rs#L122), [`core/dr-types/src/lib.rs:204`](../core/dr-types/src/lib.rs#L204), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3413`](../ui/dr-ui/src/collections_ui.rs#L3413), [`ui/dr-ui/src/collections_ui.rs:636`](../ui/dr-ui/src/collections_ui.rs#L636), [`ui/dr-ui/src/collections_ui.rs:704`](../ui/dr-ui/src/collections_ui.rs#L704), [`ui/dr-ui/src/derived_sync.rs:585`](../ui/dr-ui/src/derived_sync.rs#L585), [`ui/dr-ui/src/derived_sync.rs:805`](../ui/dr-ui/src/derived_sync.rs#L805), [`ui/dr-ui/src/library.rs:1003`](../ui/dr-ui/src/library.rs#L1003), [`ui/dr-ui/src/library.rs:2001`](../ui/dr-ui/src/library.rs#L2001), [`ui/dr-ui/src/library.rs:2091`](../ui/dr-ui/src/library.rs#L2091), [`ui/dr-ui/src/library.rs:3569`](../ui/dr-ui/src/library.rs#L3569), [`ui/dr-ui/src/library.rs:4003`](../ui/dr-ui/src/library.rs#L4003), [`ui/dr-ui/src/library_ui.rs:1408`](../ui/dr-ui/src/library_ui.rs#L1408), [`ui/dr-ui/src/library_ui.rs:1481`](../ui/dr-ui/src/library_ui.rs#L1481), [`ui/dr-ui/src/library_ui.rs:1680`](../ui/dr-ui/src/library_ui.rs#L1680), [`ui/dr-ui/src/remote.rs:40`](../ui/dr-ui/src/remote.rs#L40), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/icons.slint:261`](../ui/dr-ui/ui/icons.slint#L261) | | FR-NC-6d | [`core/dr-sync-folder/src/borrow.rs:1`](../core/dr-sync-folder/src/borrow.rs#L1), [`core/dr-sync-folder/src/lib.rs:1`](../core/dr-sync-folder/src/lib.rs#L1), [`core/dr-sync-folder/src/vfs.rs:1`](../core/dr-sync-folder/src/vfs.rs#L1) | | FR-NC-7 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:105`](../core/dr-sync-nextcloud/src/lib.rs#L105), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:3883`](../ui/dr-ui/src/library.rs#L3883), [`ui/dr-ui/src/library_ui.rs:4235`](../ui/dr-ui/src/library_ui.rs#L4235), [`ui/dr-ui/ui/settings.slint:395`](../ui/dr-ui/ui/settings.slint#L395) | | FR-NC-7a | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`core/dr-types/src/settings.rs:206`](../core/dr-types/src/settings.rs#L206), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1304`](../ui/dr-ui/src/lib.rs#L1304), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-NC-7b | [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-sync/src/upload.rs:1`](../core/dr-sync/src/upload.rs#L1), [`ui/dr-ui/src/import.rs:122`](../ui/dr-ui/src/import.rs#L122), [`ui/dr-ui/src/import.rs:336`](../ui/dr-ui/src/import.rs#L336), [`ui/dr-ui/src/import.rs:584`](../ui/dr-ui/src/import.rs#L584), [`ui/dr-ui/src/import.rs:97`](../ui/dr-ui/src/import.rs#L97), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1304`](../ui/dr-ui/src/lib.rs#L1304) | -| FR-NC-8 | [`core/dr-catalog/src/rating.rs:204`](../core/dr-catalog/src/rating.rs#L204), [`core/dr-catalog/src/rating.rs:66`](../core/dr-catalog/src/rating.rs#L66), [`core/dr-catalog/src/rating.rs:900`](../core/dr-catalog/src/rating.rs#L900), [`core/dr-catalog/src/schema.rs:155`](../core/dr-catalog/src/schema.rs#L155), [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120), [`core/dr-pipeline/src/sidecar.rs:2487`](../core/dr-pipeline/src/sidecar.rs#L2487), [`core/dr-pipeline/src/sidecar.rs:619`](../core/dr-pipeline/src/sidecar.rs#L619), [`core/dr-pipeline/src/sidecar.rs:649`](../core/dr-pipeline/src/sidecar.rs#L649), [`core/dr-pipeline/src/sidecar.rs:94`](../core/dr-pipeline/src/sidecar.rs#L94), [`ui/dr-ui/src/lib.rs:2102`](../ui/dr-ui/src/lib.rs#L2102), [`ui/dr-ui/src/library.rs:1045`](../ui/dr-ui/src/library.rs#L1045), [`ui/dr-ui/src/library.rs:2310`](../ui/dr-ui/src/library.rs#L2310), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500), [`ui/dr-ui/src/library.rs:7312`](../ui/dr-ui/src/library.rs#L7312), [`ui/dr-ui/src/library.rs:755`](../ui/dr-ui/src/library.rs#L755), [`ui/dr-ui/src/library_ui.rs:554`](../ui/dr-ui/src/library_ui.rs#L554), [`ui/dr-ui/src/presets.rs:288`](../ui/dr-ui/src/presets.rs#L288), [`ui/dr-ui/src/presets.rs:335`](../ui/dr-ui/src/presets.rs#L335) | -| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:204`](../core/dr-catalog/src/rating.rs#L204), [`core/dr-catalog/src/rating.rs:66`](../core/dr-catalog/src/rating.rs#L66), [`core/dr-catalog/src/rating.rs:900`](../core/dr-catalog/src/rating.rs#L900), [`core/dr-catalog/src/schema.rs:155`](../core/dr-catalog/src/schema.rs#L155), [`core/dr-catalog/src/schema.rs:532`](../core/dr-catalog/src/schema.rs#L532), [`core/dr-catalog/src/schema.rs:728`](../core/dr-catalog/src/schema.rs#L728), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/src/sidecar.rs:185`](../core/dr-pipeline/src/sidecar.rs#L185), [`core/dr-pipeline/src/sidecar.rs:2253`](../core/dr-pipeline/src/sidecar.rs#L2253), [`core/dr-pipeline/src/sidecar.rs:2487`](../core/dr-pipeline/src/sidecar.rs#L2487), [`core/dr-pipeline/src/sidecar.rs:354`](../core/dr-pipeline/src/sidecar.rs#L354), [`core/dr-pipeline/src/sidecar.rs:452`](../core/dr-pipeline/src/sidecar.rs#L452), [`core/dr-pipeline/src/sidecar.rs:619`](../core/dr-pipeline/src/sidecar.rs#L619), [`core/dr-pipeline/src/sidecar.rs:649`](../core/dr-pipeline/src/sidecar.rs#L649), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/mask_sidecar.rs:1055`](../core/dr-pipeline/tests/mask_sidecar.rs#L1055), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:45`](../core/dr-sync/src/scan.rs#L45), [`core/dr-sync/src/scan.rs:912`](../core/dr-sync/src/scan.rs#L912), [`ui/dr-ui/src/derived_sync.rs:585`](../ui/dr-ui/src/derived_sync.rs#L585), [`ui/dr-ui/src/library.rs:1045`](../ui/dr-ui/src/library.rs#L1045), [`ui/dr-ui/src/library.rs:1065`](../ui/dr-ui/src/library.rs#L1065), [`ui/dr-ui/src/library.rs:1376`](../ui/dr-ui/src/library.rs#L1376), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library.rs:2310`](../ui/dr-ui/src/library.rs#L2310), [`ui/dr-ui/src/library.rs:7144`](../ui/dr-ui/src/library.rs#L7144), [`ui/dr-ui/src/library.rs:7312`](../ui/dr-ui/src/library.rs#L7312), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:755`](../ui/dr-ui/src/library.rs#L755), [`ui/dr-ui/src/library.rs:899`](../ui/dr-ui/src/library.rs#L899), [`ui/dr-ui/src/library_ui.rs:1265`](../ui/dr-ui/src/library_ui.rs#L1265), [`ui/dr-ui/src/presets.rs:288`](../ui/dr-ui/src/presets.rs#L288), [`ui/dr-ui/src/presets.rs:335`](../ui/dr-ui/src/presets.rs#L335) | +| FR-NC-8 | [`core/dr-catalog/src/rating.rs:204`](../core/dr-catalog/src/rating.rs#L204), [`core/dr-catalog/src/rating.rs:66`](../core/dr-catalog/src/rating.rs#L66), [`core/dr-catalog/src/rating.rs:900`](../core/dr-catalog/src/rating.rs#L900), [`core/dr-catalog/src/schema.rs:155`](../core/dr-catalog/src/schema.rs#L155), [`core/dr-pipeline/src/sidecar.rs:120`](../core/dr-pipeline/src/sidecar.rs#L120), [`core/dr-pipeline/src/sidecar.rs:2574`](../core/dr-pipeline/src/sidecar.rs#L2574), [`core/dr-pipeline/src/sidecar.rs:619`](../core/dr-pipeline/src/sidecar.rs#L619), [`core/dr-pipeline/src/sidecar.rs:649`](../core/dr-pipeline/src/sidecar.rs#L649), [`core/dr-pipeline/src/sidecar.rs:94`](../core/dr-pipeline/src/sidecar.rs#L94), [`ui/dr-ui/src/lib.rs:2102`](../ui/dr-ui/src/lib.rs#L2102), [`ui/dr-ui/src/library.rs:1045`](../ui/dr-ui/src/library.rs#L1045), [`ui/dr-ui/src/library.rs:2310`](../ui/dr-ui/src/library.rs#L2310), [`ui/dr-ui/src/library.rs:500`](../ui/dr-ui/src/library.rs#L500), [`ui/dr-ui/src/library.rs:7312`](../ui/dr-ui/src/library.rs#L7312), [`ui/dr-ui/src/library.rs:755`](../ui/dr-ui/src/library.rs#L755), [`ui/dr-ui/src/library_ui.rs:554`](../ui/dr-ui/src/library_ui.rs#L554), [`ui/dr-ui/src/presets.rs:288`](../ui/dr-ui/src/presets.rs#L288), [`ui/dr-ui/src/presets.rs:335`](../ui/dr-ui/src/presets.rs#L335) | +| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:204`](../core/dr-catalog/src/rating.rs#L204), [`core/dr-catalog/src/rating.rs:66`](../core/dr-catalog/src/rating.rs#L66), [`core/dr-catalog/src/rating.rs:900`](../core/dr-catalog/src/rating.rs#L900), [`core/dr-catalog/src/schema.rs:155`](../core/dr-catalog/src/schema.rs#L155), [`core/dr-catalog/src/schema.rs:532`](../core/dr-catalog/src/schema.rs#L532), [`core/dr-catalog/src/schema.rs:728`](../core/dr-catalog/src/schema.rs#L728), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:158`](../core/dr-pipeline/src/sidecar.rs#L158), [`core/dr-pipeline/src/sidecar.rs:185`](../core/dr-pipeline/src/sidecar.rs#L185), [`core/dr-pipeline/src/sidecar.rs:2340`](../core/dr-pipeline/src/sidecar.rs#L2340), [`core/dr-pipeline/src/sidecar.rs:2574`](../core/dr-pipeline/src/sidecar.rs#L2574), [`core/dr-pipeline/src/sidecar.rs:354`](../core/dr-pipeline/src/sidecar.rs#L354), [`core/dr-pipeline/src/sidecar.rs:452`](../core/dr-pipeline/src/sidecar.rs#L452), [`core/dr-pipeline/src/sidecar.rs:619`](../core/dr-pipeline/src/sidecar.rs#L619), [`core/dr-pipeline/src/sidecar.rs:649`](../core/dr-pipeline/src/sidecar.rs#L649), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/mask_sidecar.rs:1055`](../core/dr-pipeline/tests/mask_sidecar.rs#L1055), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-sync/src/scan.rs:31`](../core/dr-sync/src/scan.rs#L31), [`core/dr-sync/src/scan.rs:45`](../core/dr-sync/src/scan.rs#L45), [`core/dr-sync/src/scan.rs:912`](../core/dr-sync/src/scan.rs#L912), [`ui/dr-ui/src/derived_sync.rs:585`](../ui/dr-ui/src/derived_sync.rs#L585), [`ui/dr-ui/src/library.rs:1045`](../ui/dr-ui/src/library.rs#L1045), [`ui/dr-ui/src/library.rs:1065`](../ui/dr-ui/src/library.rs#L1065), [`ui/dr-ui/src/library.rs:1376`](../ui/dr-ui/src/library.rs#L1376), [`ui/dr-ui/src/library.rs:1411`](../ui/dr-ui/src/library.rs#L1411), [`ui/dr-ui/src/library.rs:2310`](../ui/dr-ui/src/library.rs#L2310), [`ui/dr-ui/src/library.rs:7144`](../ui/dr-ui/src/library.rs#L7144), [`ui/dr-ui/src/library.rs:7312`](../ui/dr-ui/src/library.rs#L7312), [`ui/dr-ui/src/library.rs:73`](../ui/dr-ui/src/library.rs#L73), [`ui/dr-ui/src/library.rs:755`](../ui/dr-ui/src/library.rs#L755), [`ui/dr-ui/src/library.rs:899`](../ui/dr-ui/src/library.rs#L899), [`ui/dr-ui/src/library_ui.rs:1265`](../ui/dr-ui/src/library_ui.rs#L1265), [`ui/dr-ui/src/presets.rs:288`](../ui/dr-ui/src/presets.rs#L288), [`ui/dr-ui/src/presets.rs:335`](../ui/dr-ui/src/presets.rs#L335) | | FR-PLAT-AND-2 | [`core/dr-catalog/src/walk.rs:1022`](../core/dr-catalog/src/walk.rs#L1022), [`core/dr-catalog/src/walk.rs:435`](../core/dr-catalog/src/walk.rs#L435), [`core/dr-sync-folder/src/lib.rs:242`](../core/dr-sync-folder/src/lib.rs#L242), [`core/dr-sync-folder/src/tests.rs:51`](../core/dr-sync-folder/src/tests.rs#L51), [`core/dr-sync/src/error.rs:113`](../core/dr-sync/src/error.rs#L113), [`core/dr-sync/src/error.rs:195`](../core/dr-sync/src/error.rs#L195), [`core/dr-sync/src/scan.rs:200`](../core/dr-sync/src/scan.rs#L200), [`core/dr-sync/src/scan.rs:505`](../core/dr-sync/src/scan.rs#L505), [`core/dr-sync/src/scan.rs:531`](../core/dr-sync/src/scan.rs#L531), [`core/dr-sync/src/scan.rs:560`](../core/dr-sync/src/scan.rs#L560), [`ui/dr-ui/src/library.rs:1276`](../ui/dr-ui/src/library.rs#L1276), [`ui/dr-ui/src/library.rs:1329`](../ui/dr-ui/src/library.rs#L1329), [`ui/dr-ui/src/library.rs:1360`](../ui/dr-ui/src/library.rs#L1360), [`ui/dr-ui/src/library.rs:1727`](../ui/dr-ui/src/library.rs#L1727), [`ui/dr-ui/src/library_ui.rs:1241`](../ui/dr-ui/src/library_ui.rs#L1241), [`ui/dr-ui/src/library_ui.rs:1332`](../ui/dr-ui/src/library_ui.rs#L1332), [`ui/dr-ui/src/library_ui.rs:1931`](../ui/dr-ui/src/library_ui.rs#L1931), [`ui/dr-ui/src/library_ui.rs:325`](../ui/dr-ui/src/library_ui.rs#L325), [`ui/dr-ui/src/library_ui.rs:523`](../ui/dr-ui/src/library_ui.rs#L523) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | | FR-PLAT-AND-4 | [`core/dr-catalog/src/runner.rs:1`](../core/dr-catalog/src/runner.rs#L1) | -| FR-PLAT-AND-5 | [`apps/darkroom-android/src/lib.rs:166`](../apps/darkroom-android/src/lib.rs#L166), [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-gpu/src/detail.rs:161`](../core/dr-gpu/src/detail.rs#L161), [`core/dr-gpu/src/detail.rs:547`](../core/dr-gpu/src/detail.rs#L547), [`core/dr-gpu/tests/detail_stage.rs:417`](../core/dr-gpu/tests/detail_stage.rs#L417), [`ui/dr-ui/src/develop.rs:3458`](../ui/dr-ui/src/develop.rs#L3458), [`ui/dr-ui/src/identity_ui.rs:121`](../ui/dr-ui/src/identity_ui.rs#L121), [`ui/dr-ui/src/lib.rs:1123`](../ui/dr-ui/src/lib.rs#L1123), [`ui/dr-ui/src/lib.rs:1572`](../ui/dr-ui/src/lib.rs#L1572), [`ui/dr-ui/src/memory.rs:163`](../ui/dr-ui/src/memory.rs#L163), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | +| FR-PLAT-AND-5 | [`apps/darkroom-android/src/lib.rs:166`](../apps/darkroom-android/src/lib.rs#L166), [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-gpu/src/detail.rs:161`](../core/dr-gpu/src/detail.rs#L161), [`core/dr-gpu/src/detail.rs:547`](../core/dr-gpu/src/detail.rs#L547), [`core/dr-gpu/tests/detail_stage.rs:417`](../core/dr-gpu/tests/detail_stage.rs#L417), [`ui/dr-ui/src/develop.rs:3608`](../ui/dr-ui/src/develop.rs#L3608), [`ui/dr-ui/src/identity_ui.rs:121`](../ui/dr-ui/src/identity_ui.rs#L121), [`ui/dr-ui/src/lib.rs:1123`](../ui/dr-ui/src/lib.rs#L1123), [`ui/dr-ui/src/lib.rs:1572`](../ui/dr-ui/src/lib.rs#L1572), [`ui/dr-ui/src/memory.rs:163`](../ui/dr-ui/src/memory.rs#L163), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | | FR-PLAT-AND-6 | [`apps/darkroom-android/src/lib.rs:388`](../apps/darkroom-android/src/lib.rs#L388) | | FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`platform/dr-plat/src/storage.rs:344`](../platform/dr-plat/src/storage.rs#L344), [`ui/dr-ui/src/lib.rs:999`](../ui/dr-ui/src/lib.rs#L999), [`ui/dr-ui/src/preset_store.rs:1`](../ui/dr-ui/src/preset_store.rs#L1), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-PLAT-LIN-2 | [`platform/dr-plat/src/display.rs:1`](../platform/dr-plat/src/display.rs#L1), [`platform/dr-plat/src/display/wayland.rs:1`](../platform/dr-plat/src/display/wayland.rs#L1), [`platform/dr-plat/src/display/x11.rs:1`](../platform/dr-plat/src/display/x11.rs#L1) | -| FR-PLG-2 | [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/expr.rs:152`](../core/dr-pipeline/src/declared/expr.rs#L152), [`core/dr-pipeline/src/declared/expr.rs:1`](../core/dr-pipeline/src/declared/expr.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:82`](../core/dr-pipeline/src/declared/mod.rs#L82), [`core/dr-pipeline/src/descriptor.rs:15`](../core/dr-pipeline/src/descriptor.rs#L15), [`core/dr-pipeline/src/descriptor.rs:695`](../core/dr-pipeline/src/descriptor.rs#L695), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/tests/declared_parity.rs:1`](../core/dr-pipeline/tests/declared_parity.rs#L1), [`core/dr-pipeline/tests/declared_parity.rs:240`](../core/dr-pipeline/tests/declared_parity.rs#L240), [`core/dr-pipeline/tests/declared_parity.rs:305`](../core/dr-pipeline/tests/declared_parity.rs#L305), [`core/dr-pipeline/tests/declared_parity.rs:358`](../core/dr-pipeline/tests/declared_parity.rs#L358), [`core/dr-pipeline/tests/declared_parity.rs:418`](../core/dr-pipeline/tests/declared_parity.rs#L418) | -| FR-PLG-2d | [`core/dr-pipeline/src/declared/decl.rs:112`](../core/dr-pipeline/src/declared/decl.rs#L112), [`core/dr-pipeline/src/declared/decl.rs:154`](../core/dr-pipeline/src/declared/decl.rs#L154), [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/decl.rs:422`](../core/dr-pipeline/src/declared/decl.rs#L422), [`core/dr-pipeline/src/declared/decl.rs:67`](../core/dr-pipeline/src/declared/decl.rs#L67), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:384`](../core/dr-pipeline/src/declared/mod.rs#L384), [`core/dr-pipeline/src/declared/mod.rs:403`](../core/dr-pipeline/src/declared/mod.rs#L403) | -| FR-PLG-8 | [`core/dr-pipeline/src/sidecar.rs:1924`](../core/dr-pipeline/src/sidecar.rs#L1924), [`core/dr-pipeline/src/sidecar.rs:1957`](../core/dr-pipeline/src/sidecar.rs#L1957) | +| FR-PLG-2 | [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/expr.rs:152`](../core/dr-pipeline/src/declared/expr.rs#L152), [`core/dr-pipeline/src/declared/expr.rs:1`](../core/dr-pipeline/src/declared/expr.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:82`](../core/dr-pipeline/src/declared/mod.rs#L82), [`core/dr-pipeline/src/descriptor.rs:15`](../core/dr-pipeline/src/descriptor.rs#L15), [`core/dr-pipeline/src/descriptor.rs:674`](../core/dr-pipeline/src/descriptor.rs#L674), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/tests/declared_parity.rs:1`](../core/dr-pipeline/tests/declared_parity.rs#L1), [`core/dr-pipeline/tests/declared_parity.rs:240`](../core/dr-pipeline/tests/declared_parity.rs#L240), [`core/dr-pipeline/tests/declared_parity.rs:305`](../core/dr-pipeline/tests/declared_parity.rs#L305), [`core/dr-pipeline/tests/declared_parity.rs:358`](../core/dr-pipeline/tests/declared_parity.rs#L358), [`core/dr-pipeline/tests/declared_parity.rs:417`](../core/dr-pipeline/tests/declared_parity.rs#L417) | +| FR-PLG-2d | [`core/dr-pipeline/src/declared/decl.rs:112`](../core/dr-pipeline/src/declared/decl.rs#L112), [`core/dr-pipeline/src/declared/decl.rs:152`](../core/dr-pipeline/src/declared/decl.rs#L152), [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/decl.rs:420`](../core/dr-pipeline/src/declared/decl.rs#L420), [`core/dr-pipeline/src/declared/decl.rs:67`](../core/dr-pipeline/src/declared/decl.rs#L67), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:384`](../core/dr-pipeline/src/declared/mod.rs#L384), [`core/dr-pipeline/src/declared/mod.rs:403`](../core/dr-pipeline/src/declared/mod.rs#L403) | +| FR-PLG-8 | [`core/dr-pipeline/src/sidecar.rs:2011`](../core/dr-pipeline/src/sidecar.rs#L2011), [`core/dr-pipeline/src/sidecar.rs:2044`](../core/dr-pipeline/src/sidecar.rs#L2044) | | FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:132`](../core/dr-types/src/lib.rs#L132), [`core/dr-types/src/lib.rs:203`](../core/dr-types/src/lib.rs#L203) | | FR-RAW-3 | [`core/dr-decode/src/lib.rs:139`](../core/dr-decode/src/lib.rs#L139), [`core/dr-decode/src/lib.rs:506`](../core/dr-decode/src/lib.rs#L506), [`core/dr-decode/src/locate.rs:1366`](../core/dr-decode/src/locate.rs#L1366) | | FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:232`](../ui/dr-ui/src/lib.rs#L232) | | FR-RAW-5 | [`core/dr-decode/src/lib.rs:167`](../core/dr-decode/src/lib.rs#L167), [`core/dr-gpu/src/demosaic.rs:34`](../core/dr-gpu/src/demosaic.rs#L34), [`core/dr-gpu/src/demosaic.rs:602`](../core/dr-gpu/src/demosaic.rs#L602), [`core/dr-gpu/src/demosaic.rs:681`](../core/dr-gpu/src/demosaic.rs#L681), [`core/dr-gpu/src/demosaic.rs:805`](../core/dr-gpu/src/demosaic.rs#L805) | -| FR-UI-1 | [`core/dr-types/src/settings.rs:1166`](../core/dr-types/src/settings.rs#L1166), [`core/dr-types/src/settings.rs:321`](../core/dr-types/src/settings.rs#L321), [`core/dr-types/src/settings.rs:341`](../core/dr-types/src/settings.rs#L341), [`ui/dr-ui/src/lib.rs:101`](../ui/dr-ui/src/lib.rs#L101), [`ui/dr-ui/src/lib.rs:108`](../ui/dr-ui/src/lib.rs#L108), [`ui/dr-ui/src/lib.rs:2334`](../ui/dr-ui/src/lib.rs#L2334), [`ui/dr-ui/src/lib.rs:3343`](../ui/dr-ui/src/lib.rs#L3343), [`ui/dr-ui/src/lib.rs:3362`](../ui/dr-ui/src/lib.rs#L3362), [`ui/dr-ui/src/masks_ui.rs:884`](../ui/dr-ui/src/masks_ui.rs#L884), [`ui/dr-ui/src/settings_ui.rs:160`](../ui/dr-ui/src/settings_ui.rs#L160), [`ui/dr-ui/ui/app.slint:2310`](../ui/dr-ui/ui/app.slint#L2310), [`ui/dr-ui/ui/app.slint:728`](../ui/dr-ui/ui/app.slint#L728), [`ui/dr-ui/ui/app.slint:897`](../ui/dr-ui/ui/app.slint#L897), [`ui/dr-ui/ui/identity.slint:188`](../ui/dr-ui/ui/identity.slint#L188), [`ui/dr-ui/ui/library.slint:1071`](../ui/dr-ui/ui/library.slint#L1071), [`ui/dr-ui/ui/settings.slint:104`](../ui/dr-ui/ui/settings.slint#L104), [`ui/dr-ui/ui/settings.slint:565`](../ui/dr-ui/ui/settings.slint#L565), [`ui/dr-ui/ui/toolrail.slint:156`](../ui/dr-ui/ui/toolrail.slint#L156), [`ui/dr-ui/ui/toolrail.slint:89`](../ui/dr-ui/ui/toolrail.slint#L89) | +| FR-UI-1 | [`core/dr-types/src/settings.rs:1166`](../core/dr-types/src/settings.rs#L1166), [`core/dr-types/src/settings.rs:321`](../core/dr-types/src/settings.rs#L321), [`core/dr-types/src/settings.rs:341`](../core/dr-types/src/settings.rs#L341), [`ui/dr-ui/src/lib.rs:101`](../ui/dr-ui/src/lib.rs#L101), [`ui/dr-ui/src/lib.rs:108`](../ui/dr-ui/src/lib.rs#L108), [`ui/dr-ui/src/lib.rs:2334`](../ui/dr-ui/src/lib.rs#L2334), [`ui/dr-ui/src/lib.rs:3343`](../ui/dr-ui/src/lib.rs#L3343), [`ui/dr-ui/src/lib.rs:3362`](../ui/dr-ui/src/lib.rs#L3362), [`ui/dr-ui/src/masks_ui.rs:951`](../ui/dr-ui/src/masks_ui.rs#L951), [`ui/dr-ui/src/settings_ui.rs:160`](../ui/dr-ui/src/settings_ui.rs#L160), [`ui/dr-ui/ui/app.slint:2313`](../ui/dr-ui/ui/app.slint#L2313), [`ui/dr-ui/ui/app.slint:728`](../ui/dr-ui/ui/app.slint#L728), [`ui/dr-ui/ui/app.slint:897`](../ui/dr-ui/ui/app.slint#L897), [`ui/dr-ui/ui/identity.slint:188`](../ui/dr-ui/ui/identity.slint#L188), [`ui/dr-ui/ui/library.slint:1071`](../ui/dr-ui/ui/library.slint#L1071), [`ui/dr-ui/ui/settings.slint:104`](../ui/dr-ui/ui/settings.slint#L104), [`ui/dr-ui/ui/settings.slint:565`](../ui/dr-ui/ui/settings.slint#L565), [`ui/dr-ui/ui/toolrail.slint:156`](../ui/dr-ui/ui/toolrail.slint#L156), [`ui/dr-ui/ui/toolrail.slint:89`](../ui/dr-ui/ui/toolrail.slint#L89) | | FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1045`](../ui/dr-ui/src/collections_ui.rs#L1045), [`ui/dr-ui/src/collections_ui.rs:1055`](../ui/dr-ui/src/collections_ui.rs#L1055), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1569`](../ui/dr-ui/src/collections_ui.rs#L1569), [`ui/dr-ui/src/collections_ui.rs:1821`](../ui/dr-ui/src/collections_ui.rs#L1821), [`ui/dr-ui/src/collections_ui.rs:2926`](../ui/dr-ui/src/collections_ui.rs#L2926), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/lib.rs:101`](../ui/dr-ui/src/lib.rs#L101), [`ui/dr-ui/src/library_ui.rs:360`](../ui/dr-ui/src/library_ui.rs#L360), [`ui/dr-ui/src/library_ui.rs:6421`](../ui/dr-ui/src/library_ui.rs#L6421), [`ui/dr-ui/src/library_ui.rs:6566`](../ui/dr-ui/src/library_ui.rs#L6566), [`ui/dr-ui/src/library_ui.rs:7781`](../ui/dr-ui/src/library_ui.rs#L7781), [`ui/dr-ui/ui/app.slint:596`](../ui/dr-ui/ui/app.slint#L596), [`ui/dr-ui/ui/app.slint:60`](../ui/dr-ui/ui/app.slint#L60), [`ui/dr-ui/ui/app.slint:610`](../ui/dr-ui/ui/app.slint#L610), [`ui/dr-ui/ui/app.slint:979`](../ui/dr-ui/ui/app.slint#L979), [`ui/dr-ui/ui/controls.slint:593`](../ui/dr-ui/ui/controls.slint#L593), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1277`](../ui/dr-ui/ui/library.slint#L1277), [`ui/dr-ui/ui/library.slint:1284`](../ui/dr-ui/ui/library.slint#L1284), [`ui/dr-ui/ui/library.slint:1381`](../ui/dr-ui/ui/library.slint#L1381), [`ui/dr-ui/ui/library.slint:2565`](../ui/dr-ui/ui/library.slint#L2565), [`ui/dr-ui/ui/library.slint:4195`](../ui/dr-ui/ui/library.slint#L4195), [`ui/dr-ui/ui/library.slint:960`](../ui/dr-ui/ui/library.slint#L960), [`ui/dr-ui/ui/library.slint:993`](../ui/dr-ui/ui/library.slint#L993), [`ui/dr-ui/ui/presets.slint:19`](../ui/dr-ui/ui/presets.slint#L19), [`ui/dr-ui/ui/settings.slint:118`](../ui/dr-ui/ui/settings.slint#L118) | -| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2445`](../ui/dr-ui/src/develop.rs#L2445), [`ui/dr-ui/src/develop.rs:2554`](../ui/dr-ui/src/develop.rs#L2554), [`ui/dr-ui/src/library_ui.rs:5485`](../ui/dr-ui/src/library_ui.rs#L5485), [`ui/dr-ui/src/masks_ui.rs:249`](../ui/dr-ui/src/masks_ui.rs#L249), [`ui/dr-ui/src/masks_ui.rs:976`](../ui/dr-ui/src/masks_ui.rs#L976), [`ui/dr-ui/src/masks_ui.rs:998`](../ui/dr-ui/src/masks_ui.rs#L998), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2085`](../ui/dr-ui/ui/app.slint#L2085), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/masks.slint:612`](../ui/dr-ui/ui/masks.slint#L612) | -| FR-UI-4 | [`tools/traceability/src/gestures.rs:1`](../tools/traceability/src/gestures.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1045`](../ui/dr-ui/src/collections_ui.rs#L1045), [`ui/dr-ui/src/collections_ui.rs:1055`](../ui/dr-ui/src/collections_ui.rs#L1055), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:126`](../ui/dr-ui/src/collections_ui.rs#L126), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1569`](../ui/dr-ui/src/collections_ui.rs#L1569), [`ui/dr-ui/src/collections_ui.rs:1685`](../ui/dr-ui/src/collections_ui.rs#L1685), [`ui/dr-ui/src/collections_ui.rs:1821`](../ui/dr-ui/src/collections_ui.rs#L1821), [`ui/dr-ui/src/collections_ui.rs:1848`](../ui/dr-ui/src/collections_ui.rs#L1848), [`ui/dr-ui/src/collections_ui.rs:1964`](../ui/dr-ui/src/collections_ui.rs#L1964), [`ui/dr-ui/src/collections_ui.rs:2926`](../ui/dr-ui/src/collections_ui.rs#L2926), [`ui/dr-ui/src/collections_ui.rs:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:365`](../ui/dr-ui/src/collections_ui.rs#L365), [`ui/dr-ui/src/collections_ui.rs:569`](../ui/dr-ui/src/collections_ui.rs#L569), [`ui/dr-ui/src/collections_ui.rs:589`](../ui/dr-ui/src/collections_ui.rs#L589), [`ui/dr-ui/src/gesture_book.rs:3`](../ui/dr-ui/src/gesture_book.rs#L3), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/library_ui.rs:5485`](../ui/dr-ui/src/library_ui.rs#L5485), [`ui/dr-ui/src/library_ui.rs:5497`](../ui/dr-ui/src/library_ui.rs#L5497), [`ui/dr-ui/src/library_ui.rs:5567`](../ui/dr-ui/src/library_ui.rs#L5567), [`ui/dr-ui/src/library_ui.rs:5677`](../ui/dr-ui/src/library_ui.rs#L5677), [`ui/dr-ui/src/library_ui.rs:5705`](../ui/dr-ui/src/library_ui.rs#L5705), [`ui/dr-ui/src/library_ui.rs:6554`](../ui/dr-ui/src/library_ui.rs#L6554), [`ui/dr-ui/src/library_ui.rs:6566`](../ui/dr-ui/src/library_ui.rs#L6566), [`ui/dr-ui/ui/app.slint:1925`](../ui/dr-ui/ui/app.slint#L1925), [`ui/dr-ui/ui/app.slint:572`](../ui/dr-ui/ui/app.slint#L572), [`ui/dr-ui/ui/app.slint:601`](../ui/dr-ui/ui/app.slint#L601), [`ui/dr-ui/ui/app.slint:610`](../ui/dr-ui/ui/app.slint#L610), [`ui/dr-ui/ui/app.slint:695`](../ui/dr-ui/ui/app.slint#L695), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1048`](../ui/dr-ui/ui/library.slint#L1048), [`ui/dr-ui/ui/library.slint:1252`](../ui/dr-ui/ui/library.slint#L1252), [`ui/dr-ui/ui/library.slint:1277`](../ui/dr-ui/ui/library.slint#L1277), [`ui/dr-ui/ui/library.slint:1284`](../ui/dr-ui/ui/library.slint#L1284), [`ui/dr-ui/ui/library.slint:1317`](../ui/dr-ui/ui/library.slint#L1317), [`ui/dr-ui/ui/library.slint:1374`](../ui/dr-ui/ui/library.slint#L1374), [`ui/dr-ui/ui/library.slint:1381`](../ui/dr-ui/ui/library.slint#L1381), [`ui/dr-ui/ui/library.slint:2565`](../ui/dr-ui/ui/library.slint#L2565), [`ui/dr-ui/ui/library.slint:4079`](../ui/dr-ui/ui/library.slint#L4079), [`ui/dr-ui/ui/library.slint:4195`](../ui/dr-ui/ui/library.slint#L4195), [`ui/dr-ui/ui/library.slint:960`](../ui/dr-ui/ui/library.slint#L960), [`ui/dr-ui/ui/library.slint:987`](../ui/dr-ui/ui/library.slint#L987), [`ui/dr-ui/ui/library.slint:993`](../ui/dr-ui/ui/library.slint#L993) | -| FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2771`](../ui/dr-ui/src/lib.rs#L2771), [`ui/dr-ui/src/lib.rs:3409`](../ui/dr-ui/src/lib.rs#L3409), [`ui/dr-ui/src/lib.rs:3594`](../ui/dr-ui/src/lib.rs#L3594), [`ui/dr-ui/src/masks_ui.rs:931`](../ui/dr-ui/src/masks_ui.rs#L931), [`ui/dr-ui/ui/app.slint:127`](../ui/dr-ui/ui/app.slint#L127), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | +| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2458`](../ui/dr-ui/src/develop.rs#L2458), [`ui/dr-ui/src/develop.rs:2567`](../ui/dr-ui/src/develop.rs#L2567), [`ui/dr-ui/src/library_ui.rs:5485`](../ui/dr-ui/src/library_ui.rs#L5485), [`ui/dr-ui/src/masks_ui.rs:1043`](../ui/dr-ui/src/masks_ui.rs#L1043), [`ui/dr-ui/src/masks_ui.rs:1065`](../ui/dr-ui/src/masks_ui.rs#L1065), [`ui/dr-ui/src/masks_ui.rs:263`](../ui/dr-ui/src/masks_ui.rs#L263), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:2088`](../ui/dr-ui/ui/app.slint#L2088), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:747`](../ui/dr-ui/ui/collections.slint#L747), [`ui/dr-ui/ui/masks.slint:757`](../ui/dr-ui/ui/masks.slint#L757) | +| FR-UI-4 | [`tools/traceability/src/gestures.rs:1`](../tools/traceability/src/gestures.rs#L1), [`ui/dr-ui/src/collections_ui.rs:1045`](../ui/dr-ui/src/collections_ui.rs#L1045), [`ui/dr-ui/src/collections_ui.rs:1055`](../ui/dr-ui/src/collections_ui.rs#L1055), [`ui/dr-ui/src/collections_ui.rs:116`](../ui/dr-ui/src/collections_ui.rs#L116), [`ui/dr-ui/src/collections_ui.rs:117`](../ui/dr-ui/src/collections_ui.rs#L117), [`ui/dr-ui/src/collections_ui.rs:126`](../ui/dr-ui/src/collections_ui.rs#L126), [`ui/dr-ui/src/collections_ui.rs:154`](../ui/dr-ui/src/collections_ui.rs#L154), [`ui/dr-ui/src/collections_ui.rs:1552`](../ui/dr-ui/src/collections_ui.rs#L1552), [`ui/dr-ui/src/collections_ui.rs:1569`](../ui/dr-ui/src/collections_ui.rs#L1569), [`ui/dr-ui/src/collections_ui.rs:1685`](../ui/dr-ui/src/collections_ui.rs#L1685), [`ui/dr-ui/src/collections_ui.rs:1821`](../ui/dr-ui/src/collections_ui.rs#L1821), [`ui/dr-ui/src/collections_ui.rs:1848`](../ui/dr-ui/src/collections_ui.rs#L1848), [`ui/dr-ui/src/collections_ui.rs:1964`](../ui/dr-ui/src/collections_ui.rs#L1964), [`ui/dr-ui/src/collections_ui.rs:2926`](../ui/dr-ui/src/collections_ui.rs#L2926), [`ui/dr-ui/src/collections_ui.rs:2963`](../ui/dr-ui/src/collections_ui.rs#L2963), [`ui/dr-ui/src/collections_ui.rs:365`](../ui/dr-ui/src/collections_ui.rs#L365), [`ui/dr-ui/src/collections_ui.rs:569`](../ui/dr-ui/src/collections_ui.rs#L569), [`ui/dr-ui/src/collections_ui.rs:589`](../ui/dr-ui/src/collections_ui.rs#L589), [`ui/dr-ui/src/gesture_book.rs:3`](../ui/dr-ui/src/gesture_book.rs#L3), [`ui/dr-ui/src/gestures.rs:1`](../ui/dr-ui/src/gestures.rs#L1), [`ui/dr-ui/src/library_ui.rs:5485`](../ui/dr-ui/src/library_ui.rs#L5485), [`ui/dr-ui/src/library_ui.rs:5497`](../ui/dr-ui/src/library_ui.rs#L5497), [`ui/dr-ui/src/library_ui.rs:5567`](../ui/dr-ui/src/library_ui.rs#L5567), [`ui/dr-ui/src/library_ui.rs:5677`](../ui/dr-ui/src/library_ui.rs#L5677), [`ui/dr-ui/src/library_ui.rs:5705`](../ui/dr-ui/src/library_ui.rs#L5705), [`ui/dr-ui/src/library_ui.rs:6554`](../ui/dr-ui/src/library_ui.rs#L6554), [`ui/dr-ui/src/library_ui.rs:6566`](../ui/dr-ui/src/library_ui.rs#L6566), [`ui/dr-ui/ui/app.slint:1928`](../ui/dr-ui/ui/app.slint#L1928), [`ui/dr-ui/ui/app.slint:572`](../ui/dr-ui/ui/app.slint#L572), [`ui/dr-ui/ui/app.slint:601`](../ui/dr-ui/ui/app.slint#L601), [`ui/dr-ui/ui/app.slint:610`](../ui/dr-ui/ui/app.slint#L610), [`ui/dr-ui/ui/app.slint:695`](../ui/dr-ui/ui/app.slint#L695), [`ui/dr-ui/ui/gestures.slint:1`](../ui/dr-ui/ui/gestures.slint#L1), [`ui/dr-ui/ui/library.slint:1048`](../ui/dr-ui/ui/library.slint#L1048), [`ui/dr-ui/ui/library.slint:1252`](../ui/dr-ui/ui/library.slint#L1252), [`ui/dr-ui/ui/library.slint:1277`](../ui/dr-ui/ui/library.slint#L1277), [`ui/dr-ui/ui/library.slint:1284`](../ui/dr-ui/ui/library.slint#L1284), [`ui/dr-ui/ui/library.slint:1317`](../ui/dr-ui/ui/library.slint#L1317), [`ui/dr-ui/ui/library.slint:1374`](../ui/dr-ui/ui/library.slint#L1374), [`ui/dr-ui/ui/library.slint:1381`](../ui/dr-ui/ui/library.slint#L1381), [`ui/dr-ui/ui/library.slint:2565`](../ui/dr-ui/ui/library.slint#L2565), [`ui/dr-ui/ui/library.slint:4079`](../ui/dr-ui/ui/library.slint#L4079), [`ui/dr-ui/ui/library.slint:4195`](../ui/dr-ui/ui/library.slint#L4195), [`ui/dr-ui/ui/library.slint:960`](../ui/dr-ui/ui/library.slint#L960), [`ui/dr-ui/ui/library.slint:987`](../ui/dr-ui/ui/library.slint#L987), [`ui/dr-ui/ui/library.slint:993`](../ui/dr-ui/ui/library.slint#L993) | +| FR-UI-5 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/lib.rs:2771`](../ui/dr-ui/src/lib.rs#L2771), [`ui/dr-ui/src/lib.rs:3409`](../ui/dr-ui/src/lib.rs#L3409), [`ui/dr-ui/src/lib.rs:3594`](../ui/dr-ui/src/lib.rs#L3594), [`ui/dr-ui/src/masks_ui.rs:998`](../ui/dr-ui/src/masks_ui.rs#L998), [`ui/dr-ui/ui/app.slint:127`](../ui/dr-ui/ui/app.slint#L127), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | | FR-UI-6 | [`ui/dr-ui/ui/widgets.slint:1`](../ui/dr-ui/ui/widgets.slint#L1) | | FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:395`](../core/dr-pipeline/src/framing.rs#L395), [`core/dr-types/src/settings.rs:321`](../core/dr-types/src/settings.rs#L321), [`ui/dr-ui/src/lib.rs:3362`](../ui/dr-ui/src/lib.rs#L3362), [`ui/dr-ui/ui/app.slint:728`](../ui/dr-ui/ui/app.slint#L728), [`ui/dr-ui/ui/settings.slint:565`](../ui/dr-ui/ui/settings.slint#L565), [`ui/dr-ui/ui/toolrail.slint:89`](../ui/dr-ui/ui/toolrail.slint#L89) | | FR-UI-8 | [`core/dr-catalog/src/collections.rs:555`](../core/dr-catalog/src/collections.rs#L555), [`core/dr-catalog/src/collections.rs:576`](../core/dr-catalog/src/collections.rs#L576), [`core/dr-types/src/place.rs:1`](../core/dr-types/src/place.rs#L1), [`ui/dr-ui/src/derived_sync.rs:188`](../ui/dr-ui/src/derived_sync.rs#L188), [`ui/dr-ui/src/derived_sync.rs:657`](../ui/dr-ui/src/derived_sync.rs#L657), [`ui/dr-ui/src/derived_sync.rs:667`](../ui/dr-ui/src/derived_sync.rs#L667), [`ui/dr-ui/src/derived_sync.rs:749`](../ui/dr-ui/src/derived_sync.rs#L749), [`ui/dr-ui/src/derived_sync.rs:74`](../ui/dr-ui/src/derived_sync.rs#L74), [`ui/dr-ui/src/library.rs:1106`](../ui/dr-ui/src/library.rs#L1106), [`ui/dr-ui/src/library.rs:4556`](../ui/dr-ui/src/library.rs#L4556), [`ui/dr-ui/src/library.rs:5274`](../ui/dr-ui/src/library.rs#L5274), [`ui/dr-ui/src/library.rs:5340`](../ui/dr-ui/src/library.rs#L5340), [`ui/dr-ui/src/library_ui.rs:195`](../ui/dr-ui/src/library_ui.rs#L195), [`ui/dr-ui/src/library_ui.rs:2118`](../ui/dr-ui/src/library_ui.rs#L2118), [`ui/dr-ui/src/library_ui.rs:2307`](../ui/dr-ui/src/library_ui.rs#L2307), [`ui/dr-ui/src/library_ui.rs:2324`](../ui/dr-ui/src/library_ui.rs#L2324), [`ui/dr-ui/src/library_ui.rs:2422`](../ui/dr-ui/src/library_ui.rs#L2422), [`ui/dr-ui/src/library_ui.rs:5518`](../ui/dr-ui/src/library_ui.rs#L5518), [`ui/dr-ui/src/library_ui.rs:5558`](../ui/dr-ui/src/library_ui.rs#L5558), [`ui/dr-ui/src/library_ui.rs:5587`](../ui/dr-ui/src/library_ui.rs#L5587), [`ui/dr-ui/src/library_ui.rs:5850`](../ui/dr-ui/src/library_ui.rs#L5850), [`ui/dr-ui/src/library_ui.rs:6073`](../ui/dr-ui/src/library_ui.rs#L6073), [`ui/dr-ui/src/library_ui.rs:6795`](../ui/dr-ui/src/library_ui.rs#L6795), [`ui/dr-ui/src/library_ui.rs:859`](../ui/dr-ui/src/library_ui.rs#L859), [`ui/dr-ui/src/library_ui.rs:892`](../ui/dr-ui/src/library_ui.rs#L892), [`ui/dr-ui/src/library_ui.rs:938`](../ui/dr-ui/src/library_ui.rs#L938), [`ui/dr-ui/src/place.rs:1`](../ui/dr-ui/src/place.rs#L1) | | NFR-A11Y-2 | [`ui/dr-ui/tests/ui_controls_are_accessible.rs:1`](../ui/dr-ui/tests/ui_controls_are_accessible.rs#L1) | | NFR-A11Y-3 | [`ui/dr-ui/src/histogram.rs:356`](../ui/dr-ui/src/histogram.rs#L356), [`ui/dr-ui/ui/histogram.slint:137`](../ui/dr-ui/ui/histogram.slint#L137), [`ui/dr-ui/ui/library.slint:756`](../ui/dr-ui/ui/library.slint#L756), [`ui/dr-ui/ui/library.slint:904`](../ui/dr-ui/ui/library.slint#L904), [`ui/dr-ui/ui/peaking.slint:1`](../ui/dr-ui/ui/peaking.slint#L1) | | NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/library.rs:3385`](../ui/dr-ui/src/library.rs#L3385) | -| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1809`](../ui/dr-ui/src/export.rs#L1809), [`ui/dr-ui/src/export.rs:1836`](../ui/dr-ui/src/export.rs#L1836), [`ui/dr-ui/src/export.rs:412`](../ui/dr-ui/src/export.rs#L412), [`ui/dr-ui/src/export.rs:438`](../ui/dr-ui/src/export.rs#L438), [`ui/dr-ui/src/lib.rs:2468`](../ui/dr-ui/src/lib.rs#L2468), [`ui/dr-ui/ui/app.slint:1158`](../ui/dr-ui/ui/app.slint#L1158), [`ui/dr-ui/ui/library.slint:4157`](../ui/dr-ui/ui/library.slint#L4157) | +| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1809`](../ui/dr-ui/src/export.rs#L1809), [`ui/dr-ui/src/export.rs:1836`](../ui/dr-ui/src/export.rs#L1836), [`ui/dr-ui/src/export.rs:412`](../ui/dr-ui/src/export.rs#L412), [`ui/dr-ui/src/export.rs:438`](../ui/dr-ui/src/export.rs#L438), [`ui/dr-ui/src/lib.rs:2468`](../ui/dr-ui/src/lib.rs#L2468), [`ui/dr-ui/ui/app.slint:1161`](../ui/dr-ui/ui/app.slint#L1161), [`ui/dr-ui/ui/library.slint:4157`](../ui/dr-ui/ui/library.slint#L4157) | | NFR-ARCH-4 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-export/src/error.rs:1`](../core/dr-export/src/error.rs#L1), [`core/dr-thumbs/src/error.rs:1`](../core/dr-thumbs/src/error.rs#L1), [`platform/dr-plat/src/storage.rs:148`](../platform/dr-plat/src/storage.rs#L148), [`ui/dr-ui/src/export.rs:514`](../ui/dr-ui/src/export.rs#L514) | | NFR-OPS-1 | [`platform/dr-plat/src/diagnostics.rs:1`](../platform/dr-plat/src/diagnostics.rs#L1), [`platform/dr-plat/src/state.rs:1`](../platform/dr-plat/src/state.rs#L1) | | NFR-OPS-2 | [`platform/dr-plat/src/crash.rs:1`](../platform/dr-plat/src/crash.rs#L1) | | NFR-OPS-3 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | NFR-P1 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:162`](../core/dr-catalog/src/walk.rs#L162), [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`tools/bench/src/catalog_open.rs:1`](../tools/bench/src/catalog_open.rs#L1) | | NFR-P13 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) | -| NFR-P14 | [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`ui/dr-ui/src/develop.rs:3353`](../ui/dr-ui/src/develop.rs#L3353), [`ui/dr-ui/src/lib.rs:1814`](../ui/dr-ui/src/lib.rs#L1814) | +| NFR-P14 | [`core/dr-gpu/src/focus.rs:186`](../core/dr-gpu/src/focus.rs#L186), [`core/dr-gpu/src/focus.rs:1`](../core/dr-gpu/src/focus.rs#L1), [`core/dr-gpu/src/focus.rs:317`](../core/dr-gpu/src/focus.rs#L317), [`core/dr-gpu/src/shaders/focus_peak.wgsl:1`](../core/dr-gpu/src/shaders/focus_peak.wgsl#L1), [`ui/dr-ui/src/develop.rs:3503`](../ui/dr-ui/src/develop.rs#L3503), [`ui/dr-ui/src/lib.rs:1814`](../ui/dr-ui/src/lib.rs#L1814) | | NFR-P3 | [`tools/bench/src/thumbnails.rs:1`](../tools/bench/src/thumbnails.rs#L1) | | NFR-P5 | [`core/dr-catalog/src/schema.rs:356`](../core/dr-catalog/src/schema.rs#L356), [`ui/dr-ui/src/library.rs:4663`](../ui/dr-ui/src/library.rs#L4663), [`ui/dr-ui/src/library.rs:5776`](../ui/dr-ui/src/library.rs#L5776), [`ui/dr-ui/src/library_ui.rs:4725`](../ui/dr-ui/src/library_ui.rs#L4725), [`ui/dr-ui/src/library_ui.rs:64`](../ui/dr-ui/src/library_ui.rs#L64) | | NFR-P9 | [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/export.rs:992`](../ui/dr-ui/src/export.rs#L992), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1) | @@ -152,7 +153,7 @@ _None._ | NFR-R6 | [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/lib.rs:255`](../core/dr-catalog/src/lib.rs#L255), [`core/dr-catalog/src/recovery.rs:1`](../core/dr-catalog/src/recovery.rs#L1) | | NFR-R7 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) | | NFR-R8 | [`core/dr-gpu/src/error.rs:1`](../core/dr-gpu/src/error.rs#L1) | -| NFR-RES-1 | [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/develop.rs:3458`](../ui/dr-ui/src/develop.rs#L3458), [`ui/dr-ui/src/identity_ui.rs:121`](../ui/dr-ui/src/identity_ui.rs#L121), [`ui/dr-ui/src/lib.rs:93`](../ui/dr-ui/src/lib.rs#L93), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | +| NFR-RES-1 | [`core/dr-gpu/src/adjust.rs:1029`](../core/dr-gpu/src/adjust.rs#L1029), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`ui/dr-ui/src/develop.rs:3608`](../ui/dr-ui/src/develop.rs#L3608), [`ui/dr-ui/src/identity_ui.rs:121`](../ui/dr-ui/src/identity_ui.rs#L121), [`ui/dr-ui/src/lib.rs:93`](../ui/dr-ui/src/lib.rs#L93), [`ui/dr-ui/src/memory.rs:1`](../ui/dr-ui/src/memory.rs#L1) | | NFR-RES-2 | [`ui/dr-ui/src/faces.rs:346`](../ui/dr-ui/src/faces.rs#L346), [`ui/dr-ui/src/library.rs:3385`](../ui/dr-ui/src/library.rs#L3385), [`ui/dr-ui/src/library.rs:3576`](../ui/dr-ui/src/library.rs#L3576) | | NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/schema.rs:785`](../core/dr-catalog/src/schema.rs#L785), [`core/dr-gpu/src/lib.rs:93`](../core/dr-gpu/src/lib.rs#L93), [`core/dr-thumbs/src/codec.rs:1`](../core/dr-thumbs/src/codec.rs#L1), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/library.rs:3292`](../ui/dr-ui/src/library.rs#L3292) | | NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) | @@ -166,6 +167,7 @@ _None._ ## Not yet tagged 53 of 182 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built. +53 of 181 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
Show untagged requirements diff --git a/ui/dr-ui/src/develop.rs b/ui/dr-ui/src/develop.rs index 767efec..57e9527 100644 --- a/ui/dr-ui/src/develop.rs +++ b/ui/dr-ui/src/develop.rs @@ -2078,6 +2078,12 @@ impl DevelopSession { // resizes, and does not need redrawing when the view moves. let (pw, ph) = self.mask_raster_size(); let subjects = self.subjects.as_ref(); + // TRACES: FR-DEV-10 + // A refcount, taken before the rasteriser is borrowed mutably. A range + // layer measures the photograph itself, so the pass needs the source + // as well as the stack — and it is the same texture every other pass + // reads, not a copy made for masking. + let source = self.demosaiced.clone(); // **Built here, not in `segment`.** A gradient needs no segmentation — // a graduated filter over a sky never had to know what a sky is — but @@ -2104,9 +2110,16 @@ impl DevelopSession { // No label field: region masks were the watershed's, and nothing // produces one any more. A stored layer that still names regions is // skipped by the rasteriser rather than drawn wrong. - pass.render(self.graph.masks(), None, subjects, pw, ph) - .inspect_err(|e| log::warn!("mask rasterisation failed: {e}")) - .is_ok() + pass.render( + self.graph.masks(), + None, + subjects, + Some(source.as_ref()), + pw, + ph, + ) + .inspect_err(|e| log::warn!("mask rasterisation failed: {e}")) + .is_ok() } /// The size the mask array is rasterised at, in source space. @@ -2920,6 +2933,135 @@ impl DevelopSession { Some(id) } + /// TRACES: FR-DEV-10 + /// Add a mask that selects by a range of the photograph's own values. + /// + /// Needs no segmentation, like a gradient, and for a stronger reason: a + /// band is a question about the picture rather than about what is in it. + /// `chromatic` picks the colour range over the brightness one. + pub fn add_range_mask(&mut self, chromatic: bool) -> Option { + let id = self.graph.masks().next_id(); + let source = if chromatic { + MaskSource::skin_tones() + } else { + MaskSource::highlights() + }; + if !self + .graph + .masks_mut() + .push(MaskLayer::new(id.clone(), source)) + { + return None; + } + self.active_masks = vec![id.clone()]; + self.history + .record(&self.graph, Edit::Action(labels::step::MASK_ADDED)); + Some(id) + } + + /// TRACES: FR-DEV-10 + /// Move a range layer's band — its two bounds and the fade at each edge. + /// + /// All three at once, because they are one control: dragging the lower + /// bound past the upper swaps them (see `MaskSource::luminance_range`), + /// and a setter per field would have to decide that question three times + /// with only a third of the answer each time. + /// + /// A no-op on a layer that is not a range, rather than a panic: the panel + /// asks first, and a callback that arrives against a layer the user has + /// since replaced is ordinary rather than a fault. + pub fn set_mask_band(&mut self, id: &str, lo: f32, hi: f32, softness: f32) { + let Some(layer) = self.graph.masks_mut().get_mut(id) else { + return; + }; + // Built into a temporary first: the arc is read out of the layer and + // the whole source is written back over it, and the two cannot be the + // same statement. + let rebuilt = match &layer.source { + MaskSource::Luminance { .. } => MaskSource::luminance_range(lo, hi, softness), + MaskSource::Colour { hue, hue_width, .. } => { + MaskSource::colour_range(*hue, *hue_width, lo, hi, softness) + } + _ => return, + }; + layer.source = rebuilt; + // `Control`, not `Action`: a band is dragged, and a drag is one + // decision however many values it passes through. + self.history + .record(&self.graph, Edit::Control(labels::step::MASK_RANGE)); + } + + /// TRACES: FR-DEV-10 + /// Move a colour range's arc — its centre hue and its half-width. + pub fn set_mask_hue(&mut self, id: &str, hue: f32, width: f32) { + let Some(layer) = self.graph.masks_mut().get_mut(id) else { + return; + }; + // A temporary, for the reason `set_mask_band` gives. + let rebuilt = match &layer.source { + MaskSource::Colour { + chroma_lo, + chroma_hi, + softness, + .. + } => MaskSource::colour_range(hue, width, *chroma_lo, *chroma_hi, *softness), + // Only a colour range has an arc. A brightness one reaching here + // is a stale callback against a layer the user has replaced, + // which is ordinary rather than a fault. + _ => return, + }; + layer.source = rebuilt; + self.history + .record(&self.graph, Edit::Control(labels::step::MASK_RANGE)); + } + + /// TRACES: FR-DEV-10 + /// Whether the band controls apply to this layer. + pub fn mask_is_ranged(&self, id: &str) -> bool { + self.graph + .masks() + .get(id) + .is_some_and(|l| l.source.is_range()) + } + + /// TRACES: FR-DEV-10 + /// Whether this layer's band is over colour rather than over brightness. + pub fn mask_is_chromatic(&self, id: &str) -> bool { + self.graph + .masks() + .get(id) + .is_some_and(|l| matches!(l.source, MaskSource::Colour { .. })) + } + + /// TRACES: FR-DEV-10 + /// This layer's band: lower bound, upper bound, softness. + /// + /// Tone positions for a luminance layer and chroma for a colour one — + /// the same two questions asked of different measurements, which is why + /// one panel control drives both. Zeroes for a layer that has no band, + /// which the panel never draws. + pub fn mask_band(&self, id: &str) -> (f32, f32, f32) { + match self.graph.masks().get(id).map(|l| &l.source) { + Some(MaskSource::Luminance { lo, hi, softness }) => (*lo, *hi, *softness), + Some(MaskSource::Colour { + chroma_lo, + chroma_hi, + softness, + .. + }) => (*chroma_lo, *chroma_hi, *softness), + _ => (0.0, 0.0, 0.0), + } + } + + /// TRACES: FR-DEV-10 + /// A colour range's arc: centre hue and half-width, both in turns. + pub fn mask_hue(&self, id: &str) -> (f32, f32) { + match self.graph.masks().get(id).map(|l| &l.source) { + Some(MaskSource::Colour { hue, hue_width, .. }) => (*hue, *hue_width), + _ => (0.0, 0.0), + } + } + pub fn remove_mask(&mut self, id: &str) { if self.graph.masks_mut().remove(id).is_some() { self.active_masks.retain(|a| a != id); @@ -3067,6 +3209,14 @@ impl DevelopSession { /// 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. + /// + /// A range (FR-DEV-10) is excluded on stronger grounds than the gradient. + /// Feather, falloff and morphology are every one of them a function of the + /// *signed distance from a boundary*, and a range mask has no boundary: it + /// is a weighting over the photograph's values, soft everywhere the values + /// are, sharp everywhere they are. There is no outline to grow, shrink or + /// cross. Its edge is the softness of its own band, which is in the band's + /// units rather than in pixels — see `MaskSource::Luminance`. pub fn mask_is_shapeable(&self, id: &str) -> bool { use dr_pipeline::mask::MaskSource; self.graph.masks().get(id).is_some_and(|l| { diff --git a/ui/dr-ui/src/labels.rs b/ui/dr-ui/src/labels.rs index 127d623..c60644c 100644 --- a/ui/dr-ui/src/labels.rs +++ b/ui/dr-ui/src/labels.rs @@ -49,6 +49,9 @@ pub mod step { pub const MASK_FALLOFF: LocalizedKey = LocalizedKey("history.mask_falloff"); pub const MASK_MORPHOLOGY: LocalizedKey = LocalizedKey("history.mask_morphology"); pub const MASK_REFINE: LocalizedKey = LocalizedKey("history.mask_refine"); + /// TRACES: FR-DEV-10 + /// A range mask's band moved — its bounds, its softness, or its hue arc. + pub const MASK_RANGE: LocalizedKey = LocalizedKey("history.mask_range"); /// TRACES: FR-DEV-8 /// A repair's own settings changed — its radius, its source, its opacity. @@ -92,6 +95,7 @@ pub mod step { MASK_FALLOFF, MASK_MORPHOLOGY, MASK_REFINE, + MASK_RANGE, SPOT, SPOT_PLACED, SPOT_MOVED, @@ -270,6 +274,7 @@ fn catalogued(key: &str) -> Option<&'static str> { "history.mask_falloff" => "Mask Falloff", "history.mask_morphology" => "Mask Grow/Shrink", "history.mask_refine" => "Mask Refine", + "history.mask_range" => "Mask Range", "history.spot" => "Adjust Repair", "history.spot_placed" => "Add Repair", "history.spot_moved" => "Move Repair", diff --git a/ui/dr-ui/src/masks_ui.rs b/ui/dr-ui/src/masks_ui.rs index d1979d7..9627230 100644 --- a/ui/dr-ui/src/masks_ui.rs +++ b/ui/dr-ui/src/masks_ui.rs @@ -178,23 +178,37 @@ pub(crate) fn sync(window: &AppWindow, session: &Rc = s .mask_layers() .into_iter() - .map(|(id, label, enabled, selected)| MaskRow { - stale: s.mask_is_stale(&id), - adjusted: s.mask_is_adjusted(&id), - 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), - refine: s.mask_refine(&id), - refinable: s.mask_is_refinable(&id), - shapeable: s.mask_is_shapeable(&id), - id: id.into(), - label: label.into(), - enabled, - selected, + .map(|(id, label, enabled, selected)| { + // Read once each: both answer with a tuple, and asking three + // times for three fields of one would walk the stack three times + // per row per sync. + let (band_lo, band_hi, band_softness) = s.mask_band(&id); + let (hue, hue_width) = s.mask_hue(&id); + MaskRow { + stale: s.mask_is_stale(&id), + adjusted: s.mask_is_adjusted(&id), + 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), + refine: s.mask_refine(&id), + refinable: s.mask_is_refinable(&id), + shapeable: s.mask_is_shapeable(&id), + ranged: s.mask_is_ranged(&id), + chromatic: s.mask_is_chromatic(&id), + band_lo, + band_hi, + band_softness, + hue, + hue_width, + id: id.into(), + label: label.into(), + enabled, + selected, + } }) .collect(); window.set_mask_rows(ModelRc::new(VecModel::from(rows))); @@ -611,6 +625,43 @@ pub(crate) fn wire( redraw(&w); }); } + // TRACES: FR-DEV-10 + // A range mask's band, and a colour range's arc. + // + // Two handlers rather than five, because a band is one control: moving its + // lower bound past its upper swaps them, and a callback per field would + // have to answer that with a third of the answer. Each carries the whole + // of the thing it changes and the model puts it back in order. + // + // Cheap, unlike the refine below: a band is three floats in a uniform, so + // the mask redraws in the rasterisation the redraw was going to run + // anyway and nothing is rebuilt on the CPU at all. + { + let weak = window.as_weak(); + let session = session.clone(); + let redraw = redraw.clone(); + window.on_mask_band_changed(move |id, lo, hi, softness| { + let Some(w) = weak.upgrade() else { return }; + if let Some(s) = session.borrow_mut().as_mut() { + s.set_mask_band(&id, lo, hi, softness); + } + sync(&w, &session); + redraw(&w); + }); + } + { + let weak = window.as_weak(); + let session = session.clone(); + let redraw = redraw.clone(); + window.on_mask_hue_changed(move |id, hue, width| { + let Some(w) = weak.upgrade() else { return }; + if let Some(s) = session.borrow_mut().as_mut() { + s.set_mask_hue(&id, hue, width); + } + sync(&w, &session); + redraw(&w); + }); + } // The refine control. Same shape as the feather's handler, and a separate // one for the same reason every other mask control has its own: the // callback carries the layer id, so there is nothing to share. @@ -687,6 +738,22 @@ pub(crate) fn wire( redraw(&w); }); } + // TRACES: FR-DEV-10 + { + let weak = window.as_weak(); + let session = session.clone(); + let redraw = redraw.clone(); + let rows = rows.clone(); + window.on_add_range_mask(move |chromatic| { + let Some(w) = weak.upgrade() else { return }; + if let Some(s) = session.borrow_mut().as_mut() { + s.add_range_mask(chromatic); + } + sync(&w, &session); + sync_rows(&w, &rows, &session); + redraw(&w); + }); + } { let weak = window.as_weak(); let session = session.clone(); diff --git a/ui/dr-ui/ui/app.slint b/ui/dr-ui/ui/app.slint index 7c1388b..76fc238 100644 --- a/ui/dr-ui/ui/app.slint +++ b/ui/dr-ui/ui/app.slint @@ -1125,7 +1125,10 @@ export component AppWindow inherits Window { callback mask-falloff-picked(string, int); callback mask-morphology-picked(string, int); callback mask-morph-radius-changed(string, float); + callback mask-band-changed(string, float, float, float); + callback mask-hue-changed(string, float, float); callback add-gradient-mask(bool); + callback add-range-mask(bool); callback add-subject-mask(int); callback add-category-mask(string); @@ -2662,7 +2665,14 @@ in property panel-visible: true; mask-morph-radius-changed(id, v) => { root.mask-morph-radius-changed(id, v); } + mask-band-changed(id, lo, hi, soft) => { + root.mask-band-changed(id, lo, hi, soft); + } + mask-hue-changed(id, h, w) => { + root.mask-hue-changed(id, h, w); + } add-gradient(radial) => { root.add-gradient-mask(radial); } + add-range(chromatic) => { root.add-range-mask(chromatic); } add-subject(i) => { root.add-subject-mask(i); } add-category(n) => { root.add-category-mask(n); } } diff --git a/ui/dr-ui/ui/masks.slint b/ui/dr-ui/ui/masks.slint index 758b7cd..6fa8128 100644 --- a/ui/dr-ui/ui/masks.slint +++ b/ui/dr-ui/ui/masks.slint @@ -39,6 +39,26 @@ export struct MaskRow { /// does not go through the distance field, so showing them would offer /// controls that do nothing. shapeable: bool, + /// Whether the band controls apply — a mask made from a range of the + /// photograph's own values rather than from a shape (FR-DEV-10). + /// + /// The complement of `shapeable` in practice but not by definition: a + /// range has no boundary in the picture, so it has no distance from one + /// for a feather or a dilation to act on, and its edge is the softness of + /// its own band instead. + ranged: bool, + /// Whether that range is over colour rather than over brightness, so the + /// hue arc applies as well as the band. + chromatic: bool, + /// The band's lower and upper bounds, and the fade at each edge. Tone + /// positions for a brightness range, colourfulness for a colour one — + /// two readings of one control, which is why they share three fields. + band-lo: float, + band-hi: float, + band-softness: float, + /// A colour range's arc: centre and half-width, in turns. + hue: float, + hue-width: float, /// Its region ids came from a different segmentation than the one loaded, /// so applying it would produce a confidently wrong shape. stale: bool, @@ -107,6 +127,12 @@ component MaskEntry inherits Rectangle { callback falloff-picked(int); callback morphology-picked(int); callback morph-radius-changed(float); + /// Lower bound, upper bound, softness — all three, because they are one + /// control: dragging the lower past the upper swaps them, and a callback + /// per field would have to answer that with a third of the answer. + callback band-changed(float, float, float); + /// Centre hue and half-width, in turns. + callback hue-changed(float, float); callback refined(); height: layout.preferred-height; @@ -266,6 +292,90 @@ component MaskEntry inherits Rectangle { reset => { root.refine-changed(4); } } + // The band (FR-DEV-10). Above the edge controls because for a + // range there are none: a band has no boundary in the picture and + // therefore no distance from one, so `shapeable` is false and the + // softness below is the whole of this mask's edge. + // + // Degrees and percentages rather than the turns and fractions + // stored. Both are the units the quantity is discussed in, and a + // hue slider reading 0.06 is a control the photographer has to + // learn before it means anything. + if root.data.chromatic: SliderRow { + label: "Hue"; + hint: "Which colour the mask follows. Skin sits just past red."; + value: root.data.hue * 360; + default-value: 22; + minimum: 0; + maximum: 360; + changed(v) => { root.hue-changed(v / 360, root.data.hue-width); } + reset => { root.hue-changed(0.06, root.data.hue-width); } + } + + if root.data.chromatic: SliderRow { + label: "Spread"; + hint: "How far either side of that colour the mask reaches."; + value: root.data.hue-width * 360; + default-value: 18; + minimum: 0; + // Half the circle each way is the whole of it, so this is the + // widest arc that is still a selection. + maximum: 180; + changed(v) => { root.hue-changed(root.data.hue, v / 360); } + reset => { root.hue-changed(root.data.hue, 0.05); } + } + + if root.data.ranged: SliderRow { + // Named by what the end of the band *is* rather than by + // "from", which says nothing on its own and says the wrong + // thing on the colour range, where the axis is not brightness. + label: root.data.chromatic ? "Least colourful" : "Darkest"; + value: root.data.band-lo * 100; + default-value: root.data.chromatic ? 15 : 50; + minimum: 0; + maximum: 100; + changed(v) => { + root.band-changed(v / 100, root.data.band-hi, root.data.band-softness); + } + reset => { + root.band-changed(root.data.chromatic ? 0.15 : 0.5, + root.data.band-hi, + root.data.band-softness); + } + } + + if root.data.ranged: SliderRow { + label: root.data.chromatic ? "Most colourful" : "Brightest"; + value: root.data.band-hi * 100; + default-value: 100; + minimum: 0; + maximum: 100; + changed(v) => { + root.band-changed(root.data.band-lo, v / 100, root.data.band-softness); + } + reset => { + root.band-changed(root.data.band-lo, 1.0, root.data.band-softness); + } + } + + if root.data.ranged: SliderRow { + label: "Softness"; + hint: "How gradually the mask fades in at each end of the " + + "range. At zero it is a threshold, and a threshold " + + "over a photograph's own tones draws contour lines " + + "across a smooth sky."; + value: root.data.band-softness * 100; + default-value: 15; + minimum: 0; + maximum: 100; + changed(v) => { + root.band-changed(root.data.band-lo, root.data.band-hi, v / 100); + } + reset => { + root.band-changed(root.data.band-lo, root.data.band-hi, 0.15); + } + } + // 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. @@ -344,8 +454,12 @@ export component MaskPanel inherits Rectangle { callback mask-falloff-picked(string, int); callback mask-morphology-picked(string, int); callback mask-morph-radius-changed(string, float); + callback mask-band-changed(string, float, float, float); + callback mask-hue-changed(string, float, float); callback add-gradient(bool); + /// `true` asks for the colour range rather than the brightness one. + callback add-range(bool); callback add-subject(int); callback add-category(string); @@ -577,6 +691,35 @@ export component MaskPanel inherits Rectangle { } } + // Ranges, on their own row rather than beside the gradients. A + // gradient answers "where" and these answer "what", and putting four + // buttons in one row would suggest four flavours of the same thing — + // where the difference between them is the only thing a photographer + // needs to understand to reach for the right one. + // + // "Tone" and "Colour", not "luminance range" and "colour range". What + // the photographer is choosing is which property the mask follows. + HorizontalLayout { + spacing: Theme.gap-sm; + + Button { + text: "Tone"; + enabled: root.enabled; + clicked => { root.add-range(false); } + } + Button { + text: "Colour"; + enabled: root.enabled; + clicked => { root.add-range(true); } + } + } + + if root.enabled: Caption { + text: "A tone or colour mask follows the photograph's own values, " + + "so the edit blends where the picture does."; + wrap: word-wrap; + } + if root.enabled && root.masks.length == 0: Caption { text: "No local adjustments. Select a region, or add a gradient."; wrap: word-wrap; @@ -597,6 +740,8 @@ export component MaskPanel inherits Rectangle { 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); } + band-changed(lo, hi, soft) => { root.mask-band-changed(mask.id, lo, hi, soft); } + hue-changed(h, w) => { root.mask-hue-changed(mask.id, h, w); } } // No caption saying which chain the sliders below are pointed at.