From 8ab944019005c5f386ae1fa58dace2e3717e5263 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Wed, 26 Aug 2026 20:58:13 +0200 Subject: [PATCH] Put the repair tool on the photograph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A third chip beside Crop and Local, and the mode strip's own comment predicted the shape: a mode that arms a gesture on the canvas and scopes the column. Click a mark to cover it, drag the disc to move the repair, drag the source circle to say where the patch comes from, Delete to remove it. The source starts two and a half radii towards the middle of the frame, which is FR-DEV-8's automatic placement in its cheap form — dust sits on skies and skies are smooth, so it is usually right and always one drag from fixed. Two things are drawn deliberately. The circles are the size the repairs actually are, because whether a disc covers a speck is the whole judgement being made and a fixed-size dot would say nothing about it; the reach around them is padded to a touch target so a spot on a dust mark can still be picked up on a phone. And only the selected repair shows its source: a dusty sky carries a dozen, and two dozen circles with nothing saying which belongs to which is less information rather than more. The panel edits what is stored while the canvas draws what is mapped, and the two are pushed separately for that reason — a slider deriving its value from the drawn radius would move differently at different zoom levels. It is also the one panel built from SliderRow rather than a live track: a repair has no OpId to coalesce a drag under, so a row that fires once per gesture is what keeps undo one step per decision. Verified as far as this environment allows: the strip renders and the column re-scopes, photographed under XWayland. Synthetic clicks do not reach this application, so the gestures are as-written rather than as-felt, and docs/spot-removal.md says so. Co-Authored-By: Claude Opus 5 (1M context) --- core/dr-pipeline/src/spot.rs | 46 ++++++ docs/spot-removal.md | 17 +- docs/traceability.md | 76 ++++----- ui/dr-ui/src/develop.rs | 239 ++++++++++++++++++++++++++++ ui/dr-ui/src/lib.rs | 37 +++++ ui/dr-ui/src/spots_ui.rs | 292 +++++++++++++++++++++++++++++++++++ ui/dr-ui/ui/adjust.slint | 22 ++- ui/dr-ui/ui/app.slint | 243 ++++++++++++++++++++++++++++- ui/dr-ui/ui/spots.slint | 170 ++++++++++++++++++++ 9 files changed, 1088 insertions(+), 54 deletions(-) create mode 100644 ui/dr-ui/src/spots_ui.rs create mode 100644 ui/dr-ui/ui/spots.slint diff --git a/core/dr-pipeline/src/spot.rs b/core/dr-pipeline/src/spot.rs index 1c1c100..0a96031 100644 --- a/core/dr-pipeline/src/spot.rs +++ b/core/dr-pipeline/src/spot.rs @@ -89,6 +89,14 @@ pub const MAX_RADIUS: f32 = 0.5; /// a hard edge shows as a disc even when the colour underneath it is right. pub const DEFAULT_FEATHER: f32 = 0.35; +/// How far a new repair's source starts from what it repairs, in radii. +/// +/// Clear of the disc it is replacing — a source overlapping its own +/// destination would copy the mark it is removing — and close enough that on a +/// smoothly varying background it is the same background. Two and a half puts +/// a full radius of untouched photograph between the two edges. +const SOURCE_ARM: f32 = 2.5; + /// The grid every stored length and coordinate is rounded to, as a divisor. /// /// The same value and the same reasoning as [`crate::mask::Stroke`]'s grid: @@ -196,6 +204,44 @@ impl Spot { } } + /// TRACES: FR-DEV-8 + /// Where a new repair reads from, before anybody has looked at it. + /// + /// **FR-DEV-8 asks for automatic source placement, and this is the cheap + /// half of it.** The good half searches the photograph for a patch whose + /// surroundings match — a compute dispatch scoring candidate offsets, and + /// one small readback when the spot is created (`docs/spot-removal.md` + /// §8). This is what stands in for it, and it is worth having on its own + /// terms rather than as a placeholder: dust sits on skies, skies are + /// smooth, and a patch two and a half radii away is nearly always the same + /// sky. + /// + /// Towards the centre of the frame, because that is the direction with the + /// most photograph in it: a mark near an edge sourced outwards reads from + /// the border, or from outside it, where `spot_tap` clamps and the repair + /// smears. A mark *at* the centre has no such direction and is sent right, + /// which is as good as any other bearing and is at least predictable. + /// + /// The result is what gets stored, and it is never recomputed: a repair + /// whose source moved on its own when the file was reopened would be an + /// edit changing itself, and non-destructive editing means the sidecar + /// decides what the picture is. + pub fn default_offset(centre: (f32, f32), radius: f32, aspect: f32) -> (f32, f32) { + let aspect = if aspect > 0.0 { aspect } else { 1.0 }; + // In frame units, where a direction is a direction: normalised + // coordinates would bend the bearing by the aspect ratio and send a + // source off at an angle nobody chose. + let to_centre = ((0.5 - centre.0) * aspect, 0.5 - centre.1); + let length = to_centre.0.hypot(to_centre.1); + let direction = if length > 1e-4 { + (to_centre.0 / length, to_centre.1 / length) + } else { + (1.0, 0.0) + }; + let distance = radius * SOURCE_ARM; + (direction.0 * distance, direction.1 * distance) + } + /// TRACES: FR-NC-9 /// The id a spot at `centre` is given: a short base-36 hash of the position /// it was placed at. diff --git a/docs/spot-removal.md b/docs/spot-removal.md index 963a3ba..0ebd07d 100644 --- a/docs/spot-removal.md +++ b/docs/spot-removal.md @@ -519,11 +519,21 @@ the residual a clone leaves by an order of magnitude. This is the measurement that decides `K`. **S6 — The tool.** `ViewMode::spots`, the chip, placement, handles, selection, -the panel scope, deletion, history carrying the spot set, the reveal view, the -stage-one source default. +the panel scope, deletion, history carrying the spot set, the stage-one source +default. **Done, except the reveal view** — §9's second half is the one piece +of S6 not built, and it is separable: it is a view mode over the detail chain +rather than part of the tool. *Acceptance:* a dust mark on a real frame is gone in one click, the edit survives a restart, and undo takes it back. +*What was verified, and how.* Everything below the interface is under test — +the model, the sidecar, the merge, the passes, both blend modes, and undo. The +interface itself was compiled, laid out and photographed: the strip renders +`Crop | Local | Repair` and the column re-scopes. It was **not** driven, because +synthetic clicks do not reach this application (the compositor refuses them), +so the gestures in §10 are as-written rather than as-felt. A first pass with a +real pointer is the outstanding work on this stage. + **S7 — The rest of FR-DEV-8.** The scored source search (§8 stage two), and copying a spot set across a selection. @@ -549,7 +559,8 @@ means it. 1. ~~**`K = 24`?**~~ Settled by S5's measurement: 24 samples, inverse square weights, zero error on the case the mode exists for. 2. **Does the reveal view belong to spot mode only,** or is it a view mode of its own that a photographer can turn on while doing something else? It is - cheap to allow both; the risk is a mode nobody remembers turning on. + cheap to allow both; the risk is a mode nobody remembers turning on. Still + open, and now the only part of §9 unbuilt. 3. **Should a spot be clamped inside the crop?** A spot outside the current crop costs nothing to render and is invisible, and re-cropping should bring it back rather than find it deleted. Leaning strongly towards no clamp. diff --git a/docs/traceability.md b/docs/traceability.md index 017e5f8..1a302e1 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -9,8 +9,8 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| -| Source files scanned | 232 | -| TRACES tags found | 649 | +| Source files scanned | 234 | +| TRACES tags found | 669 | | Requirements defined | 177 | | Requirements covered | 91 | | **Coverage** | **51.4%** (91/177) | @@ -34,79 +34,79 @@ _None._ | ID | Tagged in | |---|---| | FR-CAT-1 | [`core/dr-catalog/src/scan.rs:1`](../core/dr-catalog/src/scan.rs#L1), [`core/dr-catalog/src/walk.rs:109`](../core/dr-catalog/src/walk.rs#L109), [`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-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200), [`core/dr-types/src/lib.rs:269`](../core/dr-types/src/lib.rs#L269), [`core/dr-types/src/lib.rs:302`](../core/dr-types/src/lib.rs#L302), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479), [`tools/traceability/src/lib.rs:511`](../tools/traceability/src/lib.rs#L511), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1) | -| FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1002`](../ui/dr-ui/src/lib.rs#L1002), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1007`](../ui/dr-ui/ui/library.slint#L1007), [`ui/dr-ui/ui/library.slint:1161`](../ui/dr-ui/ui/library.slint#L1161), [`ui/dr-ui/ui/library.slint:857`](../ui/dr-ui/ui/library.slint#L857) | -| FR-CAT-11 | [`core/dr-catalog/src/dedup.rs:1`](../core/dr-catalog/src/dedup.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:392`](../core/dr-ingest/src/lib.rs#L392), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1002`](../ui/dr-ui/src/lib.rs#L1002), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2122`](../ui/dr-ui/src/library.rs#L2122), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | +| FR-CAT-10 | [`core/dr-ingest/src/layout.rs:1`](../core/dr-ingest/src/layout.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:733`](../core/dr-ingest/src/lib.rs#L733), [`core/dr-types/src/settings.rs:116`](../core/dr-types/src/settings.rs#L116), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1008`](../ui/dr-ui/src/lib.rs#L1008), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5), [`ui/dr-ui/ui/library.slint:1007`](../ui/dr-ui/ui/library.slint#L1007), [`ui/dr-ui/ui/library.slint:1161`](../ui/dr-ui/ui/library.slint#L1161), [`ui/dr-ui/ui/library.slint:857`](../ui/dr-ui/ui/library.slint#L857) | +| FR-CAT-11 | [`core/dr-catalog/src/dedup.rs:1`](../core/dr-catalog/src/dedup.rs#L1), [`core/dr-ingest/src/lib.rs:1`](../core/dr-ingest/src/lib.rs#L1), [`core/dr-ingest/src/lib.rs:392`](../core/dr-ingest/src/lib.rs#L392), [`core/dr-sync/src/upload.rs:40`](../core/dr-sync/src/upload.rs#L40), [`ui/dr-ui/src/import.rs:1`](../ui/dr-ui/src/import.rs#L1), [`ui/dr-ui/src/import_ui.rs:1`](../ui/dr-ui/src/import_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1008`](../ui/dr-ui/src/lib.rs#L1008), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:2122`](../ui/dr-ui/src/library.rs#L2122), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) | | FR-CAT-12 | [`core/dr-pipeline/src/sidecar.rs:121`](../core/dr-pipeline/src/sidecar.rs#L121) | | FR-CAT-13 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1) | | FR-CAT-15 | [`core/dr-catalog/src/schema.rs:422`](../core/dr-catalog/src/schema.rs#L422), [`core/dr-catalog/src/trash.rs:1`](../core/dr-catalog/src/trash.rs#L1), [`core/dr-sync-nextcloud/src/lib.rs:447`](../core/dr-sync-nextcloud/src/lib.rs#L447), [`core/dr-sync/src/lib.rs:124`](../core/dr-sync/src/lib.rs#L124), [`core/dr-sync/src/scan.rs:426`](../core/dr-sync/src/scan.rs#L426), [`core/dr-sync/src/scan.rs:57`](../core/dr-sync/src/scan.rs#L57), [`core/dr-thumbs/src/lib.rs:376`](../core/dr-thumbs/src/lib.rs#L376), [`ui/dr-ui/src/collections_ui.rs:1225`](../ui/dr-ui/src/collections_ui.rs#L1225), [`ui/dr-ui/src/collections_ui.rs:1995`](../ui/dr-ui/src/collections_ui.rs#L1995), [`ui/dr-ui/src/library.rs:152`](../ui/dr-ui/src/library.rs#L152), [`ui/dr-ui/src/library.rs:169`](../ui/dr-ui/src/library.rs#L169), [`ui/dr-ui/src/library.rs:198`](../ui/dr-ui/src/library.rs#L198), [`ui/dr-ui/src/library.rs:3007`](../ui/dr-ui/src/library.rs#L3007), [`ui/dr-ui/src/library.rs:3041`](../ui/dr-ui/src/library.rs#L3041), [`ui/dr-ui/src/library_ui.rs:184`](../ui/dr-ui/src/library_ui.rs#L184), [`ui/dr-ui/src/library_ui.rs:757`](../ui/dr-ui/src/library_ui.rs#L757), [`ui/dr-ui/src/trash.rs:1`](../ui/dr-ui/src/trash.rs#L1), [`ui/dr-ui/ui/collections.slint:572`](../ui/dr-ui/ui/collections.slint#L572) | | FR-CAT-1a | [`core/dr-catalog/src/walk.rs:1`](../core/dr-catalog/src/walk.rs#L1), [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53) | | FR-CAT-2 | [`core/dr-catalog/src/lib.rs:1`](../core/dr-catalog/src/lib.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.rs#L1), [`tools/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) | -| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:69`](../core/dr-sync/src/scan.rs#L69), [`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), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489), [`ui/dr-ui/src/library.rs:2598`](../ui/dr-ui/src/library.rs#L2598), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:171`](../ui/dr-ui/src/library_ui.rs#L171), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/src/library_ui.rs:4574`](../ui/dr-ui/src/library_ui.rs#L4574), [`ui/dr-ui/ui/app.slint:513`](../ui/dr-ui/ui/app.slint#L513), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) | +| FR-CAT-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`core/dr-catalog/src/walk.rs:66`](../core/dr-catalog/src/walk.rs#L66), [`core/dr-sync/src/scan.rs:69`](../core/dr-sync/src/scan.rs#L69), [`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), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464), [`ui/dr-ui/src/import.rs:489`](../ui/dr-ui/src/import.rs#L489), [`ui/dr-ui/src/library.rs:2598`](../ui/dr-ui/src/library.rs#L2598), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:171`](../ui/dr-ui/src/library_ui.rs#L171), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/src/library_ui.rs:4574`](../ui/dr-ui/src/library_ui.rs#L4574), [`ui/dr-ui/ui/app.slint:516`](../ui/dr-ui/ui/app.slint#L516), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) | | FR-CAT-4 | [`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/schema.rs:292`](../core/dr-catalog/src/schema.rs#L292), [`ui/dr-ui/src/library.rs:179`](../ui/dr-ui/src/library.rs#L179), [`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) | -| 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:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/schema.rs:840`](../core/dr-catalog/src/schema.rs#L840), [`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:138`](../core/dr-pipeline/src/sidecar.rs#L138), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3055`](../ui/dr-ui/src/library.rs#L3055), [`ui/dr-ui/src/library_ui.rs:6255`](../ui/dr-ui/src/library_ui.rs#L6255), [`ui/dr-ui/src/library_ui.rs:6266`](../ui/dr-ui/src/library_ui.rs#L6266), [`ui/dr-ui/src/library_ui.rs:6279`](../ui/dr-ui/src/library_ui.rs#L6279), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6294`](../ui/dr-ui/src/library_ui.rs#L6294), [`ui/dr-ui/src/library_ui.rs:6303`](../ui/dr-ui/src/library_ui.rs#L6303), [`ui/dr-ui/ui/app.slint:458`](../ui/dr-ui/ui/app.slint#L458), [`ui/dr-ui/ui/app.slint:587`](../ui/dr-ui/ui/app.slint#L587), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:1212`](../ui/dr-ui/ui/library.slint#L1212), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) | -| 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:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`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:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:3301`](../ui/dr-ui/src/library.rs#L3301), [`ui/dr-ui/src/library_ui.rs:315`](../ui/dr-ui/src/library_ui.rs#L315), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5177`](../ui/dr-ui/src/library_ui.rs#L5177), [`ui/dr-ui/src/library_ui.rs:6395`](../ui/dr-ui/src/library_ui.rs#L6395), [`ui/dr-ui/ui/app.slint:461`](../ui/dr-ui/ui/app.slint#L461), [`ui/dr-ui/ui/app.slint:587`](../ui/dr-ui/ui/app.slint#L587), [`ui/dr-ui/ui/app.slint:826`](../ui/dr-ui/ui/app.slint#L826), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1278`](../ui/dr-ui/ui/library.slint#L1278), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901), [`ui/dr-ui/ui/settings.slint:115`](../ui/dr-ui/ui/settings.slint#L115) | -| 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:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3466`](../ui/dr-ui/src/library_ui.rs#L3466), [`ui/dr-ui/src/library_ui.rs:4164`](../ui/dr-ui/src/library_ui.rs#L4164), [`ui/dr-ui/ui/app.slint:582`](../ui/dr-ui/ui/app.slint#L582), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2516`](../ui/dr-ui/ui/library.slint#L2516), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) | -| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:136`](../core/dr-pipeline/src/ops/curve.rs#L136), [`core/dr-pipeline/src/ops/curve.rs:652`](../core/dr-pipeline/src/ops/curve.rs#L652), [`core/dr-pipeline/src/sidecar.rs:1536`](../core/dr-pipeline/src/sidecar.rs#L1536), [`core/dr-pipeline/src/sidecar.rs:91`](../core/dr-pipeline/src/sidecar.rs#L91), [`core/dr-pipeline/tests/tone_curve.rs:33`](../core/dr-pipeline/tests/tone_curve.rs#L33), [`ui/dr-ui/src/develop.rs:2763`](../ui/dr-ui/src/develop.rs#L2763), [`ui/dr-ui/src/develop.rs:2791`](../ui/dr-ui/src/develop.rs#L2791), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1239`](../ui/dr-ui/src/lib.rs#L1239), [`ui/dr-ui/src/lib.rs:1579`](../ui/dr-ui/src/lib.rs#L1579), [`ui/dr-ui/src/lib.rs:1684`](../ui/dr-ui/src/lib.rs#L1684), [`ui/dr-ui/src/lib.rs:464`](../ui/dr-ui/src/lib.rs#L464), [`ui/dr-ui/src/lib.rs:849`](../ui/dr-ui/src/lib.rs#L849), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4866`](../ui/dr-ui/src/library_ui.rs#L4866), [`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:394`](../core/dr-catalog/src/schema.rs#L394), [`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:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2326`](../ui/dr-ui/src/develop.rs#L2326), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library.rs:1582`](../ui/dr-ui/src/library.rs#L1582), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3408`](../ui/dr-ui/src/library.rs#L3408), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library_ui.rs:1564`](../ui/dr-ui/src/library_ui.rs#L1564), [`ui/dr-ui/src/library_ui.rs:1590`](../ui/dr-ui/src/library_ui.rs#L1590), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1700`](../ui/dr-ui/src/library_ui.rs#L1700), [`ui/dr-ui/src/library_ui.rs:226`](../ui/dr-ui/src/library_ui.rs#L226), [`ui/dr-ui/src/library_ui.rs:2316`](../ui/dr-ui/src/library_ui.rs#L2316), [`ui/dr-ui/src/library_ui.rs:259`](../ui/dr-ui/src/library_ui.rs#L259), [`ui/dr-ui/src/library_ui.rs:2754`](../ui/dr-ui/src/library_ui.rs#L2754), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:3257`](../ui/dr-ui/src/library_ui.rs#L3257), [`ui/dr-ui/src/library_ui.rs:3329`](../ui/dr-ui/src/library_ui.rs#L3329), [`ui/dr-ui/src/library_ui.rs:3517`](../ui/dr-ui/src/library_ui.rs#L3517), [`ui/dr-ui/src/library_ui.rs:3635`](../ui/dr-ui/src/library_ui.rs#L3635), [`ui/dr-ui/src/library_ui.rs:440`](../ui/dr-ui/src/library_ui.rs#L440), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:5222`](../ui/dr-ui/src/library_ui.rs#L5222), [`ui/dr-ui/src/library_ui.rs:5337`](../ui/dr-ui/src/library_ui.rs#L5337), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| 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:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/schema.rs:840`](../core/dr-catalog/src/schema.rs#L840), [`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:138`](../core/dr-pipeline/src/sidecar.rs#L138), [`ui/dr-ui/src/collections_ui.rs:1578`](../ui/dr-ui/src/collections_ui.rs#L1578), [`ui/dr-ui/src/collections_ui.rs:1597`](../ui/dr-ui/src/collections_ui.rs#L1597), [`ui/dr-ui/src/collections_ui.rs:243`](../ui/dr-ui/src/collections_ui.rs#L243), [`ui/dr-ui/src/collections_ui.rs:340`](../ui/dr-ui/src/collections_ui.rs#L340), [`ui/dr-ui/src/collections_ui.rs:89`](../ui/dr-ui/src/collections_ui.rs#L89), [`ui/dr-ui/src/library.rs:3055`](../ui/dr-ui/src/library.rs#L3055), [`ui/dr-ui/src/library_ui.rs:6255`](../ui/dr-ui/src/library_ui.rs#L6255), [`ui/dr-ui/src/library_ui.rs:6266`](../ui/dr-ui/src/library_ui.rs#L6266), [`ui/dr-ui/src/library_ui.rs:6279`](../ui/dr-ui/src/library_ui.rs#L6279), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6294`](../ui/dr-ui/src/library_ui.rs#L6294), [`ui/dr-ui/src/library_ui.rs:6303`](../ui/dr-ui/src/library_ui.rs#L6303), [`ui/dr-ui/ui/app.slint:461`](../ui/dr-ui/ui/app.slint#L461), [`ui/dr-ui/ui/app.slint:590`](../ui/dr-ui/ui/app.slint#L590), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:1212`](../ui/dr-ui/ui/library.slint#L1212), [`ui/dr-ui/ui/library.slint:18`](../ui/dr-ui/ui/library.slint#L18), [`ui/dr-ui/ui/library.slint:850`](../ui/dr-ui/ui/library.slint#L850), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901) | +| 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:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-types/src/selector.rs:1`](../core/dr-types/src/selector.rs#L1), [`core/dr-types/src/settings.rs:63`](../core/dr-types/src/settings.rs#L63), [`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:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:3301`](../ui/dr-ui/src/library.rs#L3301), [`ui/dr-ui/src/library_ui.rs:315`](../ui/dr-ui/src/library_ui.rs#L315), [`ui/dr-ui/src/library_ui.rs:387`](../ui/dr-ui/src/library_ui.rs#L387), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5177`](../ui/dr-ui/src/library_ui.rs#L5177), [`ui/dr-ui/src/library_ui.rs:6395`](../ui/dr-ui/src/library_ui.rs#L6395), [`ui/dr-ui/ui/app.slint:464`](../ui/dr-ui/ui/app.slint#L464), [`ui/dr-ui/ui/app.slint:590`](../ui/dr-ui/ui/app.slint#L590), [`ui/dr-ui/ui/app.slint:829`](../ui/dr-ui/ui/app.slint#L829), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1278`](../ui/dr-ui/ui/library.slint#L1278), [`ui/dr-ui/ui/library.slint:901`](../ui/dr-ui/ui/library.slint#L901), [`ui/dr-ui/ui/settings.slint:115`](../ui/dr-ui/ui/settings.slint#L115) | +| 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:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:1`](../ui/dr-ui/src/collections_ui.rs#L1), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library_ui.rs:3466`](../ui/dr-ui/src/library_ui.rs#L3466), [`ui/dr-ui/src/library_ui.rs:4164`](../ui/dr-ui/src/library_ui.rs#L4164), [`ui/dr-ui/ui/app.slint:585`](../ui/dr-ui/ui/app.slint#L585), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2516`](../ui/dr-ui/ui/library.slint#L2516), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) | +| FR-CAT-8 | [`core/dr-pipeline/src/ops/curve.rs:136`](../core/dr-pipeline/src/ops/curve.rs#L136), [`core/dr-pipeline/src/ops/curve.rs:652`](../core/dr-pipeline/src/ops/curve.rs#L652), [`core/dr-pipeline/src/sidecar.rs:1536`](../core/dr-pipeline/src/sidecar.rs#L1536), [`core/dr-pipeline/src/sidecar.rs:91`](../core/dr-pipeline/src/sidecar.rs#L91), [`core/dr-pipeline/tests/tone_curve.rs:33`](../core/dr-pipeline/tests/tone_curve.rs#L33), [`ui/dr-ui/src/develop.rs:3002`](../ui/dr-ui/src/develop.rs#L3002), [`ui/dr-ui/src/develop.rs:3030`](../ui/dr-ui/src/develop.rs#L3030), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1245`](../ui/dr-ui/src/lib.rs#L1245), [`ui/dr-ui/src/lib.rs:1597`](../ui/dr-ui/src/lib.rs#L1597), [`ui/dr-ui/src/lib.rs:1702`](../ui/dr-ui/src/lib.rs#L1702), [`ui/dr-ui/src/lib.rs:470`](../ui/dr-ui/src/lib.rs#L470), [`ui/dr-ui/src/lib.rs:855`](../ui/dr-ui/src/lib.rs#L855), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library_ui.rs:4866`](../ui/dr-ui/src/library_ui.rs#L4866), [`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:394`](../core/dr-catalog/src/schema.rs#L394), [`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:704`](../core/dr-catalog/src/walk.rs#L704), [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`ui/dr-ui/src/develop.rs:2565`](../ui/dr-ui/src/develop.rs#L2565), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library.rs:1582`](../ui/dr-ui/src/library.rs#L1582), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3408`](../ui/dr-ui/src/library.rs#L3408), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:680`](../ui/dr-ui/src/library.rs#L680), [`ui/dr-ui/src/library.rs:696`](../ui/dr-ui/src/library.rs#L696), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library_ui.rs:1564`](../ui/dr-ui/src/library_ui.rs#L1564), [`ui/dr-ui/src/library_ui.rs:1590`](../ui/dr-ui/src/library_ui.rs#L1590), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:1700`](../ui/dr-ui/src/library_ui.rs#L1700), [`ui/dr-ui/src/library_ui.rs:226`](../ui/dr-ui/src/library_ui.rs#L226), [`ui/dr-ui/src/library_ui.rs:2316`](../ui/dr-ui/src/library_ui.rs#L2316), [`ui/dr-ui/src/library_ui.rs:259`](../ui/dr-ui/src/library_ui.rs#L259), [`ui/dr-ui/src/library_ui.rs:2754`](../ui/dr-ui/src/library_ui.rs#L2754), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:3257`](../ui/dr-ui/src/library_ui.rs#L3257), [`ui/dr-ui/src/library_ui.rs:3329`](../ui/dr-ui/src/library_ui.rs#L3329), [`ui/dr-ui/src/library_ui.rs:3517`](../ui/dr-ui/src/library_ui.rs#L3517), [`ui/dr-ui/src/library_ui.rs:3635`](../ui/dr-ui/src/library_ui.rs#L3635), [`ui/dr-ui/src/library_ui.rs:440`](../ui/dr-ui/src/library_ui.rs#L440), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/library_ui.rs:5222`](../ui/dr-ui/src/library_ui.rs#L5222), [`ui/dr-ui/src/library_ui.rs:5337`](../ui/dr-ui/src/library_ui.rs#L5337), [`ui/dr-ui/src/presets.rs:326`](../ui/dr-ui/src/presets.rs#L326), [`ui/dr-ui/src/presets.rs:338`](../ui/dr-ui/src/presets.rs#L338), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-CULL-1 | [`core/dr-decode/src/preview.rs:134`](../core/dr-decode/src/preview.rs#L134) | | FR-CULL-2 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`ui/dr-ui/src/import.rs:464`](../ui/dr-ui/src/import.rs#L464) | | FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:138`](../core/dr-pipeline/src/sidecar.rs#L138), [`ui/dr-ui/src/library.rs:203`](../ui/dr-ui/src/library.rs#L203), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364) | | FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) | -| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`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/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:165`](../core/dr-pipeline/src/graph.rs#L165), [`core/dr-pipeline/src/graph.rs:479`](../core/dr-pipeline/src/graph.rs#L479), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:479`](../core/dr-pipeline/src/operation.rs#L479), [`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:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`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:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:1536`](../core/dr-pipeline/src/sidecar.rs#L1536), [`core/dr-pipeline/src/sidecar.rs:1592`](../core/dr-pipeline/src/sidecar.rs#L1592), [`core/dr-pipeline/src/sidecar.rs:159`](../core/dr-pipeline/src/sidecar.rs#L159), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1111`](../ui/dr-ui/src/develop.rs#L1111), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1585`](../ui/dr-ui/src/develop.rs#L1585), [`ui/dr-ui/src/develop.rs:1600`](../ui/dr-ui/src/develop.rs#L1600), [`ui/dr-ui/src/develop.rs:1622`](../ui/dr-ui/src/develop.rs#L1622), [`ui/dr-ui/src/develop.rs:1754`](../ui/dr-ui/src/develop.rs#L1754), [`ui/dr-ui/src/develop.rs:1832`](../ui/dr-ui/src/develop.rs#L1832), [`ui/dr-ui/src/develop.rs:238`](../ui/dr-ui/src/develop.rs#L238), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:2763`](../ui/dr-ui/src/develop.rs#L2763), [`ui/dr-ui/src/develop.rs:3195`](../ui/dr-ui/src/develop.rs#L3195), [`ui/dr-ui/src/develop.rs:3249`](../ui/dr-ui/src/develop.rs#L3249), [`ui/dr-ui/src/develop.rs:3293`](../ui/dr-ui/src/develop.rs#L3293), [`ui/dr-ui/src/develop.rs:3343`](../ui/dr-ui/src/develop.rs#L3343), [`ui/dr-ui/src/develop.rs:506`](../ui/dr-ui/src/develop.rs#L506), [`ui/dr-ui/src/develop.rs:544`](../ui/dr-ui/src/develop.rs#L544), [`ui/dr-ui/src/lib.rs:1275`](../ui/dr-ui/src/lib.rs#L1275), [`ui/dr-ui/src/lib.rs:1934`](../ui/dr-ui/src/lib.rs#L1934), [`ui/dr-ui/src/lib.rs:290`](../ui/dr-ui/src/lib.rs#L290), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:218`](../ui/dr-ui/src/segmentation.rs#L218), [`ui/dr-ui/ui/app.slint:1915`](../ui/dr-ui/ui/app.slint#L1915), [`ui/dr-ui/ui/app.slint:913`](../ui/dr-ui/ui/app.slint#L913) | -| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:19`](../core/dr-pipeline/src/graph.rs#L19), [`core/dr-pipeline/src/graph.rs:239`](../core/dr-pipeline/src/graph.rs#L239), [`core/dr-pipeline/src/graph.rs:41`](../core/dr-pipeline/src/graph.rs#L41), [`core/dr-pipeline/src/graph.rs:54`](../core/dr-pipeline/src/graph.rs#L54), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1008`](../ui/dr-ui/src/develop.rs#L1008), [`ui/dr-ui/src/lib.rs:579`](../ui/dr-ui/src/lib.rs#L579) | +| FR-DEV-3 | [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`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/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/framing.rs:602`](../core/dr-pipeline/src/framing.rs#L602), [`core/dr-pipeline/src/graph.rs:165`](../core/dr-pipeline/src/graph.rs#L165), [`core/dr-pipeline/src/graph.rs:479`](../core/dr-pipeline/src/graph.rs#L479), [`core/dr-pipeline/src/mask.rs:120`](../core/dr-pipeline/src/mask.rs#L120), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:479`](../core/dr-pipeline/src/operation.rs#L479), [`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:207`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L207), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:218`](../core/dr-pipeline/src/ops/curve.rs#L218), [`core/dr-pipeline/src/ops/curve.rs:631`](../core/dr-pipeline/src/ops/curve.rs#L631), [`core/dr-pipeline/src/ops/curve.rs:99`](../core/dr-pipeline/src/ops/curve.rs#L99), [`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:270`](../core/dr-pipeline/src/ops/noise_reduction.rs#L270), [`core/dr-pipeline/src/sidecar.rs:1536`](../core/dr-pipeline/src/sidecar.rs#L1536), [`core/dr-pipeline/src/sidecar.rs:1592`](../core/dr-pipeline/src/sidecar.rs#L1592), [`core/dr-pipeline/src/sidecar.rs:159`](../core/dr-pipeline/src/sidecar.rs#L159), [`core/dr-pipeline/tests/tone_curve.rs:1`](../core/dr-pipeline/tests/tone_curve.rs#L1), [`ui/dr-ui/src/develop.rs:101`](../ui/dr-ui/src/develop.rs#L101), [`ui/dr-ui/src/develop.rs:1121`](../ui/dr-ui/src/develop.rs#L1121), [`ui/dr-ui/src/develop.rs:132`](../ui/dr-ui/src/develop.rs#L132), [`ui/dr-ui/src/develop.rs:1595`](../ui/dr-ui/src/develop.rs#L1595), [`ui/dr-ui/src/develop.rs:1610`](../ui/dr-ui/src/develop.rs#L1610), [`ui/dr-ui/src/develop.rs:1632`](../ui/dr-ui/src/develop.rs#L1632), [`ui/dr-ui/src/develop.rs:1764`](../ui/dr-ui/src/develop.rs#L1764), [`ui/dr-ui/src/develop.rs:1842`](../ui/dr-ui/src/develop.rs#L1842), [`ui/dr-ui/src/develop.rs:238`](../ui/dr-ui/src/develop.rs#L238), [`ui/dr-ui/src/develop.rs:270`](../ui/dr-ui/src/develop.rs#L270), [`ui/dr-ui/src/develop.rs:3002`](../ui/dr-ui/src/develop.rs#L3002), [`ui/dr-ui/src/develop.rs:3434`](../ui/dr-ui/src/develop.rs#L3434), [`ui/dr-ui/src/develop.rs:3488`](../ui/dr-ui/src/develop.rs#L3488), [`ui/dr-ui/src/develop.rs:3532`](../ui/dr-ui/src/develop.rs#L3532), [`ui/dr-ui/src/develop.rs:3582`](../ui/dr-ui/src/develop.rs#L3582), [`ui/dr-ui/src/develop.rs:506`](../ui/dr-ui/src/develop.rs#L506), [`ui/dr-ui/src/develop.rs:553`](../ui/dr-ui/src/develop.rs#L553), [`ui/dr-ui/src/lib.rs:1281`](../ui/dr-ui/src/lib.rs#L1281), [`ui/dr-ui/src/lib.rs:1952`](../ui/dr-ui/src/lib.rs#L1952), [`ui/dr-ui/src/lib.rs:291`](../ui/dr-ui/src/lib.rs#L291), [`ui/dr-ui/src/library.rs:411`](../ui/dr-ui/src/library.rs#L411), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:41`](../ui/dr-ui/src/masks_ui.rs#L41), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/segmentation.rs:218`](../ui/dr-ui/src/segmentation.rs#L218), [`ui/dr-ui/ui/app.slint:1987`](../ui/dr-ui/ui/app.slint#L1987), [`ui/dr-ui/ui/app.slint:916`](../ui/dr-ui/ui/app.slint#L916) | +| FR-DEV-3a | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:117`](../core/dr-pipeline/src/descriptor.rs#L117), [`core/dr-pipeline/src/descriptor.rs:157`](../core/dr-pipeline/src/descriptor.rs#L157), [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/descriptor.rs:232`](../core/dr-pipeline/src/descriptor.rs#L232), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:19`](../core/dr-pipeline/src/graph.rs#L19), [`core/dr-pipeline/src/graph.rs:239`](../core/dr-pipeline/src/graph.rs#L239), [`core/dr-pipeline/src/graph.rs:41`](../core/dr-pipeline/src/graph.rs#L41), [`core/dr-pipeline/src/graph.rs:54`](../core/dr-pipeline/src/graph.rs#L54), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328), [`core/dr-pipeline/src/ops/curve.rs:318`](../core/dr-pipeline/src/ops/curve.rs#L318), [`ui/dr-ui/src/develop.rs:1018`](../ui/dr-ui/src/develop.rs#L1018), [`ui/dr-ui/src/lib.rs:585`](../ui/dr-ui/src/lib.rs#L585) | | FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259), [`core/dr-pipeline/src/graph.rs:54`](../core/dr-pipeline/src/graph.rs#L54), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) | -| FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:239`](../core/dr-pipeline/src/graph.rs#L239), [`core/dr-pipeline/src/graph.rs:41`](../core/dr-pipeline/src/graph.rs#L41), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:3922`](../ui/dr-ui/src/develop.rs#L3922) | +| FR-DEV-3c | [`core/dr-pipeline/build.rs:1807`](../core/dr-pipeline/build.rs#L1807), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:239`](../core/dr-pipeline/src/graph.rs#L239), [`core/dr-pipeline/src/graph.rs:41`](../core/dr-pipeline/src/graph.rs#L41), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:4161`](../ui/dr-ui/src/develop.rs#L4161) | | FR-DEV-3d | [`core/dr-gpu/src/adjust.rs:1041`](../core/dr-gpu/src/adjust.rs#L1041), [`core/dr-gpu/src/adjust.rs:104`](../core/dr-gpu/src/adjust.rs#L104), [`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:476`](../core/dr-gpu/tests/local_contrast.rs#L476), [`core/dr-gpu/tests/noise_reduction.rs:556`](../core/dr-gpu/tests/noise_reduction.rs#L556), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/graph.rs:518`](../core/dr-pipeline/src/graph.rs#L518), [`core/dr-pipeline/src/operation.rs:31`](../core/dr-pipeline/src/operation.rs#L31), [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352), [`core/dr-pipeline/src/operation.rs:52`](../core/dr-pipeline/src/operation.rs#L52), [`core/dr-pipeline/src/operation.rs:70`](../core/dr-pipeline/src/operation.rs#L70) | | 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:1424`](../core/dr-pipeline/src/operation.rs#L1424), [`core/dr-pipeline/src/operation.rs:1505`](../core/dr-pipeline/src/operation.rs#L1505), [`core/dr-pipeline/src/operation.rs:1530`](../core/dr-pipeline/src/operation.rs#L1530), [`core/dr-pipeline/src/operation.rs:1545`](../core/dr-pipeline/src/operation.rs#L1545), [`core/dr-pipeline/src/operation.rs:1569`](../core/dr-pipeline/src/operation.rs#L1569), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`core/dr-pipeline/src/operation.rs:403`](../core/dr-pipeline/src/operation.rs#L403), [`core/dr-pipeline/src/operation.rs:413`](../core/dr-pipeline/src/operation.rs#L413), [`core/dr-pipeline/src/operation.rs:563`](../core/dr-pipeline/src/operation.rs#L563) | -| 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/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:88`](../core/dr-film/src/grain.rs#L88), [`core/dr-film/src/lib.rs:158`](../core/dr-film/src/lib.rs#L158), [`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:120`](../core/dr-pipeline/src/graph.rs#L120), [`core/dr-pipeline/src/graph.rs:313`](../core/dr-pipeline/src/graph.rs#L313), [`core/dr-pipeline/src/graph.rs:97`](../core/dr-pipeline/src/graph.rs#L97), [`core/dr-pipeline/src/operation.rs:1028`](../core/dr-pipeline/src/operation.rs#L1028), [`core/dr-pipeline/src/operation.rs:1057`](../core/dr-pipeline/src/operation.rs#L1057), [`core/dr-pipeline/src/operation.rs:1424`](../core/dr-pipeline/src/operation.rs#L1424), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`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:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`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:300`](../core/dr-pipeline/src/ops/film_sim.rs#L300), [`core/dr-pipeline/src/ops/film_sim.rs:68`](../core/dr-pipeline/src/ops/film_sim.rs#L68), [`core/dr-pipeline/src/sidecar.rs:110`](../core/dr-pipeline/src/sidecar.rs#L110), [`core/dr-pipeline/src/sidecar.rs:170`](../core/dr-pipeline/src/sidecar.rs#L170), [`core/dr-pipeline/src/sidecar.rs:1840`](../core/dr-pipeline/src/sidecar.rs#L1840), [`core/dr-pipeline/src/sidecar.rs:1916`](../core/dr-pipeline/src/sidecar.rs#L1916), [`core/dr-pipeline/src/sidecar.rs:511`](../core/dr-pipeline/src/sidecar.rs#L511), [`core/dr-pipeline/src/sidecar.rs:652`](../core/dr-pipeline/src/sidecar.rs#L652), [`core/dr-pipeline/src/sidecar.rs:784`](../core/dr-pipeline/src/sidecar.rs#L784), [`ui/dr-ui/src/develop.rs:2341`](../ui/dr-ui/src/develop.rs#L2341), [`ui/dr-ui/src/develop.rs:2358`](../ui/dr-ui/src/develop.rs#L2358), [`ui/dr-ui/src/develop.rs:2389`](../ui/dr-ui/src/develop.rs#L2389), [`ui/dr-ui/src/develop.rs:2486`](../ui/dr-ui/src/develop.rs#L2486), [`ui/dr-ui/src/develop.rs:2800`](../ui/dr-ui/src/develop.rs#L2800), [`ui/dr-ui/src/lib.rs:1908`](../ui/dr-ui/src/lib.rs#L1908), [`ui/dr-ui/src/lib.rs:512`](../ui/dr-ui/src/lib.rs#L512), [`ui/dr-ui/src/lib.rs:570`](../ui/dr-ui/src/lib.rs#L570), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:879`](../ui/dr-ui/ui/adjust.slint#L879), [`ui/dr-ui/ui/adjust.slint:949`](../ui/dr-ui/ui/adjust.slint#L949), [`ui/dr-ui/ui/app.slint:2370`](../ui/dr-ui/ui/app.slint#L2370), [`ui/dr-ui/ui/app.slint:707`](../ui/dr-ui/ui/app.slint#L707) | +| 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/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:88`](../core/dr-film/src/grain.rs#L88), [`core/dr-film/src/lib.rs:158`](../core/dr-film/src/lib.rs#L158), [`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:120`](../core/dr-pipeline/src/graph.rs#L120), [`core/dr-pipeline/src/graph.rs:313`](../core/dr-pipeline/src/graph.rs#L313), [`core/dr-pipeline/src/graph.rs:97`](../core/dr-pipeline/src/graph.rs#L97), [`core/dr-pipeline/src/operation.rs:1028`](../core/dr-pipeline/src/operation.rs#L1028), [`core/dr-pipeline/src/operation.rs:1057`](../core/dr-pipeline/src/operation.rs#L1057), [`core/dr-pipeline/src/operation.rs:1424`](../core/dr-pipeline/src/operation.rs#L1424), [`core/dr-pipeline/src/operation.rs:265`](../core/dr-pipeline/src/operation.rs#L265), [`core/dr-pipeline/src/operation.rs:279`](../core/dr-pipeline/src/operation.rs#L279), [`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:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`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:300`](../core/dr-pipeline/src/ops/film_sim.rs#L300), [`core/dr-pipeline/src/ops/film_sim.rs:68`](../core/dr-pipeline/src/ops/film_sim.rs#L68), [`core/dr-pipeline/src/sidecar.rs:110`](../core/dr-pipeline/src/sidecar.rs#L110), [`core/dr-pipeline/src/sidecar.rs:170`](../core/dr-pipeline/src/sidecar.rs#L170), [`core/dr-pipeline/src/sidecar.rs:1840`](../core/dr-pipeline/src/sidecar.rs#L1840), [`core/dr-pipeline/src/sidecar.rs:1916`](../core/dr-pipeline/src/sidecar.rs#L1916), [`core/dr-pipeline/src/sidecar.rs:511`](../core/dr-pipeline/src/sidecar.rs#L511), [`core/dr-pipeline/src/sidecar.rs:652`](../core/dr-pipeline/src/sidecar.rs#L652), [`core/dr-pipeline/src/sidecar.rs:784`](../core/dr-pipeline/src/sidecar.rs#L784), [`ui/dr-ui/src/develop.rs:2580`](../ui/dr-ui/src/develop.rs#L2580), [`ui/dr-ui/src/develop.rs:2597`](../ui/dr-ui/src/develop.rs#L2597), [`ui/dr-ui/src/develop.rs:2628`](../ui/dr-ui/src/develop.rs#L2628), [`ui/dr-ui/src/develop.rs:2725`](../ui/dr-ui/src/develop.rs#L2725), [`ui/dr-ui/src/develop.rs:3039`](../ui/dr-ui/src/develop.rs#L3039), [`ui/dr-ui/src/lib.rs:1926`](../ui/dr-ui/src/lib.rs#L1926), [`ui/dr-ui/src/lib.rs:518`](../ui/dr-ui/src/lib.rs#L518), [`ui/dr-ui/src/lib.rs:576`](../ui/dr-ui/src/lib.rs#L576), [`ui/dr-ui/src/library.rs:402`](../ui/dr-ui/src/library.rs#L402), [`ui/dr-ui/src/library.rs:651`](../ui/dr-ui/src/library.rs#L651), [`ui/dr-ui/src/presets.rs:275`](../ui/dr-ui/src/presets.rs#L275), [`ui/dr-ui/ui/adjust.slint:889`](../ui/dr-ui/ui/adjust.slint#L889), [`ui/dr-ui/ui/adjust.slint:959`](../ui/dr-ui/ui/adjust.slint#L959), [`ui/dr-ui/ui/app.slint:2599`](../ui/dr-ui/ui/app.slint#L2599), [`ui/dr-ui/ui/app.slint:710`](../ui/dr-ui/ui/app.slint#L710) | | 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:202`](../core/dr-pipeline/src/framing.rs#L202), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336) | | FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) | -| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:140`](../core/dr-pipeline/src/history.rs#L140), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2818`](../ui/dr-ui/src/develop.rs#L2818), [`ui/dr-ui/src/develop.rs:2828`](../ui/dr-ui/src/develop.rs#L2828), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1267`](../ui/dr-ui/src/lib.rs#L1267) | -| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:2757`](../ui/dr-ui/src/develop.rs#L2757), [`ui/dr-ui/src/develop.rs:2778`](../ui/dr-ui/src/develop.rs#L2778), [`ui/dr-ui/src/lib.rs:1239`](../ui/dr-ui/src/lib.rs#L1239), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:535`](../ui/dr-ui/src/settings_ui.rs#L535), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1305`](../ui/dr-ui/ui/library.slint#L1305), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:912`](../ui/dr-ui/ui/library.slint#L912), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) | -| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`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:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:109`](../core/dr-pipeline/src/graph.rs#L109), [`core/dr-pipeline/src/graph.rs:187`](../core/dr-pipeline/src/graph.rs#L187), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`core/dr-pipeline/src/history.rs:140`](../core/dr-pipeline/src/history.rs#L140), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:517`](../core/dr-pipeline/src/operation.rs#L517), [`core/dr-pipeline/src/sidecar.rs:186`](../core/dr-pipeline/src/sidecar.rs#L186), [`core/dr-pipeline/src/sidecar.rs:330`](../core/dr-pipeline/src/sidecar.rs#L330), [`core/dr-pipeline/src/sidecar.rs:664`](../core/dr-pipeline/src/sidecar.rs#L664), [`core/dr-pipeline/src/sidecar.rs:800`](../core/dr-pipeline/src/sidecar.rs#L800), [`core/dr-pipeline/src/sidecar.rs:854`](../core/dr-pipeline/src/sidecar.rs#L854), [`core/dr-pipeline/src/sidecar.rs:884`](../core/dr-pipeline/src/sidecar.rs#L884), [`core/dr-pipeline/src/spot.rs:107`](../core/dr-pipeline/src/spot.rs#L107), [`core/dr-pipeline/src/spot.rs:143`](../core/dr-pipeline/src/spot.rs#L143), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:341`](../core/dr-pipeline/src/spot.rs#L341), [`core/dr-pipeline/src/spot.rs:426`](../core/dr-pipeline/src/spot.rs#L426), [`core/dr-pipeline/src/spot.rs:536`](../core/dr-pipeline/src/spot.rs#L536), [`core/dr-pipeline/src/spot.rs:627`](../core/dr-pipeline/src/spot.rs#L627), [`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) | -| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`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:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`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:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`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:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:449`](../core/dr-pipeline/src/graph.rs#L449), [`core/dr-pipeline/src/graph.rs:479`](../core/dr-pipeline/src/graph.rs#L479), [`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:654`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L654), [`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:662`](../core/dr-pipeline/src/ops/local_contrast.rs#L662), [`core/dr-pipeline/src/ops/noise_reduction.rs:695`](../core/dr-pipeline/src/ops/noise_reduction.rs#L695), [`core/dr-pipeline/src/spot.rs:627`](../core/dr-pipeline/src/spot.rs#L627), [`ui/dr-ui/src/develop.rs:2162`](../ui/dr-ui/src/develop.rs#L2162), [`ui/dr-ui/src/develop.rs:2984`](../ui/dr-ui/src/develop.rs#L2984), [`ui/dr-ui/src/develop.rs:3467`](../ui/dr-ui/src/develop.rs#L3467), [`ui/dr-ui/src/develop.rs:3501`](../ui/dr-ui/src/develop.rs#L3501), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59), [`ui/dr-ui/src/lib.rs:720`](../ui/dr-ui/src/lib.rs#L720) | +| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:140`](../core/dr-pipeline/src/history.rs#L140), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:3057`](../ui/dr-ui/src/develop.rs#L3057), [`ui/dr-ui/src/develop.rs:3067`](../ui/dr-ui/src/develop.rs#L3067), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1273`](../ui/dr-ui/src/lib.rs#L1273) | +| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:2996`](../ui/dr-ui/src/develop.rs#L2996), [`ui/dr-ui/src/develop.rs:3017`](../ui/dr-ui/src/develop.rs#L3017), [`ui/dr-ui/src/lib.rs:1245`](../ui/dr-ui/src/lib.rs#L1245), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:535`](../ui/dr-ui/src/settings_ui.rs#L535), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1305`](../ui/dr-ui/ui/library.slint#L1305), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:912`](../ui/dr-ui/ui/library.slint#L912), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) | +| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`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:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:109`](../core/dr-pipeline/src/graph.rs#L109), [`core/dr-pipeline/src/graph.rs:187`](../core/dr-pipeline/src/graph.rs#L187), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`core/dr-pipeline/src/history.rs:140`](../core/dr-pipeline/src/history.rs#L140), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:517`](../core/dr-pipeline/src/operation.rs#L517), [`core/dr-pipeline/src/sidecar.rs:186`](../core/dr-pipeline/src/sidecar.rs#L186), [`core/dr-pipeline/src/sidecar.rs:330`](../core/dr-pipeline/src/sidecar.rs#L330), [`core/dr-pipeline/src/sidecar.rs:664`](../core/dr-pipeline/src/sidecar.rs#L664), [`core/dr-pipeline/src/sidecar.rs:800`](../core/dr-pipeline/src/sidecar.rs#L800), [`core/dr-pipeline/src/sidecar.rs:854`](../core/dr-pipeline/src/sidecar.rs#L854), [`core/dr-pipeline/src/sidecar.rs:884`](../core/dr-pipeline/src/sidecar.rs#L884), [`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/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:1912`](../ui/dr-ui/src/develop.rs#L1912), [`ui/dr-ui/src/develop.rs:1949`](../ui/dr-ui/src/develop.rs#L1949), [`ui/dr-ui/src/develop.rs:2014`](../ui/dr-ui/src/develop.rs#L2014), [`ui/dr-ui/src/develop.rs:2096`](../ui/dr-ui/src/develop.rs#L2096), [`ui/dr-ui/src/develop.rs:2109`](../ui/dr-ui/src/develop.rs#L2109), [`ui/dr-ui/src/develop.rs:536`](../ui/dr-ui/src/develop.rs#L536), [`ui/dr-ui/src/lib.rs:1288`](../ui/dr-ui/src/lib.rs#L1288), [`ui/dr-ui/src/lib.rs:2188`](../ui/dr-ui/src/lib.rs#L2188), [`ui/dr-ui/src/lib.rs:296`](../ui/dr-ui/src/lib.rs#L296), [`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:676`](../ui/dr-ui/ui/adjust.slint#L676), [`ui/dr-ui/ui/app.slint:1722`](../ui/dr-ui/ui/app.slint#L1722), [`ui/dr-ui/ui/app.slint:2239`](../ui/dr-ui/ui/app.slint#L2239), [`ui/dr-ui/ui/app.slint:2505`](../ui/dr-ui/ui/app.slint#L2505), [`ui/dr-ui/ui/app.slint:317`](../ui/dr-ui/ui/app.slint#L317), [`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:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`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:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`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:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`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:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:449`](../core/dr-pipeline/src/graph.rs#L449), [`core/dr-pipeline/src/graph.rs:479`](../core/dr-pipeline/src/graph.rs#L479), [`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:654`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L654), [`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:662`](../core/dr-pipeline/src/ops/local_contrast.rs#L662), [`core/dr-pipeline/src/ops/noise_reduction.rs:695`](../core/dr-pipeline/src/ops/noise_reduction.rs#L695), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2401`](../ui/dr-ui/src/develop.rs#L2401), [`ui/dr-ui/src/develop.rs:3223`](../ui/dr-ui/src/develop.rs#L3223), [`ui/dr-ui/src/develop.rs:3706`](../ui/dr-ui/src/develop.rs#L3706), [`ui/dr-ui/src/develop.rs:3740`](../ui/dr-ui/src/develop.rs#L3740), [`ui/dr-ui/src/lib.rs:60`](../ui/dr-ui/src/lib.rs#L60), [`ui/dr-ui/src/lib.rs:726`](../ui/dr-ui/src/lib.rs#L726) | | FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1) | -| 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:2207`](../ui/dr-ui/src/develop.rs#L2207), [`ui/dr-ui/src/develop.rs:4569`](../ui/dr-ui/src/develop.rs#L4569), [`ui/dr-ui/src/develop.rs:4601`](../ui/dr-ui/src/develop.rs#L4601), [`ui/dr-ui/src/develop.rs:499`](../ui/dr-ui/src/develop.rs#L499), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1326`](../ui/dr-ui/src/lib.rs#L1326), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`ui/dr-ui/ui/app.slint:276`](../ui/dr-ui/ui/app.slint#L276), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | +| 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:2446`](../ui/dr-ui/src/develop.rs#L2446), [`ui/dr-ui/src/develop.rs:4808`](../ui/dr-ui/src/develop.rs#L4808), [`ui/dr-ui/src/develop.rs:4840`](../ui/dr-ui/src/develop.rs#L4840), [`ui/dr-ui/src/develop.rs:499`](../ui/dr-ui/src/develop.rs#L499), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1344`](../ui/dr-ui/src/lib.rs#L1344), [`ui/dr-ui/src/lib.rs:286`](../ui/dr-ui/src/lib.rs#L286), [`ui/dr-ui/ui/app.slint:277`](../ui/dr-ui/ui/app.slint#L277), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) | | 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:2398`](../core/dr-gpu/src/adjust.rs#L2398), [`core/dr-pipeline/src/graph.rs:439`](../core/dr-pipeline/src/graph.rs#L439), [`core/dr-pipeline/src/graph.rs:493`](../core/dr-pipeline/src/graph.rs#L493), [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`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:595`](../core/dr-types/src/settings.rs#L595), [`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:1`](../core/dr-export/src/lib.rs#L1), [`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-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-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:328`](../ui/dr-ui/src/lib.rs#L328), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:48`](../ui/dr-ui/src/settings_ui.rs#L48), [`ui/dr-ui/src/settings_ui.rs:590`](../ui/dr-ui/src/settings_ui.rs#L590) | -| 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:944`](../ui/dr-ui/src/export.rs#L944), [`ui/dr-ui/src/lib.rs:175`](../ui/dr-ui/src/lib.rs#L175), [`ui/dr-ui/src/lib.rs:1850`](../ui/dr-ui/src/lib.rs#L1850), [`ui/dr-ui/src/lib.rs:328`](../ui/dr-ui/src/lib.rs#L328), [`ui/dr-ui/src/lib.rs:363`](../ui/dr-ui/src/lib.rs#L363), [`ui/dr-ui/src/lib.rs:390`](../ui/dr-ui/src/lib.rs#L390), [`ui/dr-ui/src/library_ui.rs:3346`](../ui/dr-ui/src/library_ui.rs#L3346), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`ui/dr-ui/src/library_ui.rs:6237`](../ui/dr-ui/src/library_ui.rs#L6237), [`ui/dr-ui/src/library_ui.rs:6314`](../ui/dr-ui/src/library_ui.rs#L6314), [`ui/dr-ui/src/library_ui.rs:6326`](../ui/dr-ui/src/library_ui.rs#L6326), [`ui/dr-ui/src/library_ui.rs:660`](../ui/dr-ui/src/library_ui.rs#L660), [`ui/dr-ui/src/library_ui.rs:717`](../ui/dr-ui/src/library_ui.rs#L717), [`ui/dr-ui/ui/app.slint:1004`](../ui/dr-ui/ui/app.slint#L1004), [`ui/dr-ui/ui/app.slint:1384`](../ui/dr-ui/ui/app.slint#L1384), [`ui/dr-ui/ui/library.slint:1313`](../ui/dr-ui/ui/library.slint#L1313), [`ui/dr-ui/ui/library.slint:835`](../ui/dr-ui/ui/library.slint#L835), [`ui/dr-ui/ui/library.slint:927`](../ui/dr-ui/ui/library.slint#L927) | +| 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:334`](../ui/dr-ui/src/lib.rs#L334), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1), [`ui/dr-ui/src/settings_ui.rs:48`](../ui/dr-ui/src/settings_ui.rs#L48), [`ui/dr-ui/src/settings_ui.rs:590`](../ui/dr-ui/src/settings_ui.rs#L590) | +| 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:944`](../ui/dr-ui/src/export.rs#L944), [`ui/dr-ui/src/lib.rs:176`](../ui/dr-ui/src/lib.rs#L176), [`ui/dr-ui/src/lib.rs:1868`](../ui/dr-ui/src/lib.rs#L1868), [`ui/dr-ui/src/lib.rs:334`](../ui/dr-ui/src/lib.rs#L334), [`ui/dr-ui/src/lib.rs:369`](../ui/dr-ui/src/lib.rs#L369), [`ui/dr-ui/src/lib.rs:396`](../ui/dr-ui/src/lib.rs#L396), [`ui/dr-ui/src/library_ui.rs:3346`](../ui/dr-ui/src/library_ui.rs#L3346), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`ui/dr-ui/src/library_ui.rs:6237`](../ui/dr-ui/src/library_ui.rs#L6237), [`ui/dr-ui/src/library_ui.rs:6314`](../ui/dr-ui/src/library_ui.rs#L6314), [`ui/dr-ui/src/library_ui.rs:6326`](../ui/dr-ui/src/library_ui.rs#L6326), [`ui/dr-ui/src/library_ui.rs:660`](../ui/dr-ui/src/library_ui.rs#L660), [`ui/dr-ui/src/library_ui.rs:717`](../ui/dr-ui/src/library_ui.rs#L717), [`ui/dr-ui/ui/app.slint:1052`](../ui/dr-ui/ui/app.slint#L1052), [`ui/dr-ui/ui/app.slint:1432`](../ui/dr-ui/ui/app.slint#L1432), [`ui/dr-ui/ui/library.slint:1313`](../ui/dr-ui/ui/library.slint#L1313), [`ui/dr-ui/ui/library.slint:835`](../ui/dr-ui/ui/library.slint#L835), [`ui/dr-ui/ui/library.slint:927`](../ui/dr-ui/ui/library.slint#L927) | | 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:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/settings.rs:313`](../core/dr-types/src/settings.rs#L313), [`ui/dr-ui/src/export.rs:620`](../ui/dr-ui/src/export.rs#L620), [`ui/dr-ui/src/export.rs:648`](../ui/dr-ui/src/export.rs#L648), [`ui/dr-ui/src/export.rs:779`](../ui/dr-ui/src/export.rs#L779), [`ui/dr-ui/src/export.rs:796`](../ui/dr-ui/src/export.rs#L796), [`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:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2285`](../ui/dr-ui/src/develop.rs#L2285), [`ui/dr-ui/src/lib.rs:328`](../ui/dr-ui/src/lib.rs#L328) | +| 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:1068`](../core/dr-gpu/src/adjust.rs#L1068), [`ui/dr-ui/src/develop.rs:2524`](../ui/dr-ui/src/develop.rs#L2524), [`ui/dr-ui/src/lib.rs:334`](../ui/dr-ui/src/lib.rs#L334) | | 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/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`ui/dr-ui/src/launch.rs:256`](../ui/dr-ui/src/launch.rs#L256), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49), [`ui/dr-ui/src/launch_ui.rs:344`](../ui/dr-ui/src/launch_ui.rs#L344) | -| FR-NC-10 | [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:390`](../ui/dr-ui/src/lib.rs#L390), [`ui/dr-ui/src/library.rs:1582`](../ui/dr-ui/src/library.rs#L1582), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:947`](../ui/dr-ui/src/library.rs#L947), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:3346`](../ui/dr-ui/src/library_ui.rs#L3346), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | +| FR-NC-10 | [`ui/dr-ui/src/export.rs:1`](../ui/dr-ui/src/export.rs#L1), [`ui/dr-ui/src/lib.rs:396`](../ui/dr-ui/src/lib.rs#L396), [`ui/dr-ui/src/library.rs:1582`](../ui/dr-ui/src/library.rs#L1582), [`ui/dr-ui/src/library.rs:448`](../ui/dr-ui/src/library.rs#L448), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:947`](../ui/dr-ui/src/library.rs#L947), [`ui/dr-ui/src/library_ui.rs:1606`](../ui/dr-ui/src/library_ui.rs#L1606), [`ui/dr-ui/src/library_ui.rs:3346`](../ui/dr-ui/src/library_ui.rs#L3346), [`ui/dr-ui/src/library_ui.rs:498`](../ui/dr-ui/src/library_ui.rs#L498), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) | | FR-NC-12 | [`core/dr-sync-nextcloud/src/lib.rs:34`](../core/dr-sync-nextcloud/src/lib.rs#L34), [`core/dr-sync-nextcloud/src/lib.rs:892`](../core/dr-sync-nextcloud/src/lib.rs#L892), [`core/dr-sync/src/lib.rs:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/lib.rs:40`](../core/dr-sync/src/lib.rs#L40), [`core/dr-sync/src/reachability.rs:1`](../core/dr-sync/src/reachability.rs#L1) | | FR-NC-2 | [`core/dr-sync-nextcloud/src/session.rs:128`](../core/dr-sync-nextcloud/src/session.rs#L128), [`core/dr-sync-nextcloud/src/session.rs:34`](../core/dr-sync-nextcloud/src/session.rs#L34) | -| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2598`](../ui/dr-ui/src/library.rs#L2598), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/src/library_ui.rs:4574`](../ui/dr-ui/src/library_ui.rs#L4574), [`ui/dr-ui/ui/app.slint:513`](../ui/dr-ui/ui/app.slint#L513), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) | +| FR-NC-3 | [`core/dr-decode/src/locate.rs:1`](../core/dr-decode/src/locate.rs#L1), [`core/dr-decode/src/preview.rs:161`](../core/dr-decode/src/preview.rs#L161), [`core/dr-sync/src/capability.rs:41`](../core/dr-sync/src/capability.rs#L41), [`core/dr-thumbs/src/lib.rs:1`](../core/dr-thumbs/src/lib.rs#L1), [`ui/dr-ui/src/library.rs:1`](../ui/dr-ui/src/library.rs#L1), [`ui/dr-ui/src/library.rs:2598`](../ui/dr-ui/src/library.rs#L2598), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:1`](../ui/dr-ui/src/library_ui.rs#L1), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/src/library_ui.rs:4574`](../ui/dr-ui/src/library_ui.rs#L4574), [`ui/dr-ui/ui/app.slint:516`](../ui/dr-ui/ui/app.slint#L516), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340), [`ui/dr-ui/ui/settings.slint:72`](../ui/dr-ui/ui/settings.slint#L72) | | FR-NC-4 | [`core/dr-sync-nextcloud/src/propfind.rs:100`](../core/dr-sync-nextcloud/src/propfind.rs#L100), [`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:157`](../core/dr-sync/src/lib.rs#L157), [`core/dr-sync/src/scan.rs:93`](../core/dr-sync/src/scan.rs#L93), [`ui/dr-ui/src/launch.rs:49`](../ui/dr-ui/src/launch.rs#L49) | | 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:489`](../ui/dr-ui/src/import.rs#L489) | | 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/schema.rs:394`](../core/dr-catalog/src/schema.rs#L394), [`core/dr-catalog/src/schema.rs:795`](../core/dr-catalog/src/schema.rs#L795), [`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:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1610`](../ui/dr-ui/src/lib.rs#L1610), [`ui/dr-ui/src/lib.rs:2407`](../ui/dr-ui/src/lib.rs#L2407), [`ui/dr-ui/src/library.rs:1324`](../ui/dr-ui/src/library.rs#L1324), [`ui/dr-ui/src/library.rs:1347`](../ui/dr-ui/src/library.rs#L1347), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1412`](../ui/dr-ui/src/library_ui.rs#L1412), [`ui/dr-ui/src/library_ui.rs:1531`](../ui/dr-ui/src/library_ui.rs#L1531), [`ui/dr-ui/src/library_ui.rs:2058`](../ui/dr-ui/src/library_ui.rs#L2058), [`ui/dr-ui/src/library_ui.rs:267`](../ui/dr-ui/src/library_ui.rs#L267), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:298`](../ui/dr-ui/src/library_ui.rs#L298), [`ui/dr-ui/src/library_ui.rs:307`](../ui/dr-ui/src/library_ui.rs#L307), [`ui/dr-ui/src/library_ui.rs:399`](../ui/dr-ui/src/library_ui.rs#L399), [`ui/dr-ui/src/library_ui.rs:409`](../ui/dr-ui/src/library_ui.rs#L409), [`ui/dr-ui/src/library_ui.rs:451`](../ui/dr-ui/src/library_ui.rs#L451), [`ui/dr-ui/src/library_ui.rs:514`](../ui/dr-ui/src/library_ui.rs#L514), [`ui/dr-ui/src/library_ui.rs:5239`](../ui/dr-ui/src/library_ui.rs#L5239), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:545`](../ui/dr-ui/src/library_ui.rs#L545), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`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:2420`](../ui/dr-ui/ui/app.slint#L2420), [`ui/dr-ui/ui/app.slint:576`](../ui/dr-ui/ui/app.slint#L576), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:973`](../ui/dr-ui/ui/library.slint#L973) | +| FR-NC-6a | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:394`](../core/dr-catalog/src/schema.rs#L394), [`core/dr-catalog/src/schema.rs:795`](../core/dr-catalog/src/schema.rs#L795), [`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:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/lib.rs:1628`](../ui/dr-ui/src/lib.rs#L1628), [`ui/dr-ui/src/lib.rs:2444`](../ui/dr-ui/src/lib.rs#L2444), [`ui/dr-ui/src/library.rs:1324`](../ui/dr-ui/src/library.rs#L1324), [`ui/dr-ui/src/library.rs:1347`](../ui/dr-ui/src/library.rs#L1347), [`ui/dr-ui/src/library.rs:1505`](../ui/dr-ui/src/library.rs#L1505), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/src/library_ui.rs:1239`](../ui/dr-ui/src/library_ui.rs#L1239), [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294), [`ui/dr-ui/src/library_ui.rs:1412`](../ui/dr-ui/src/library_ui.rs#L1412), [`ui/dr-ui/src/library_ui.rs:1531`](../ui/dr-ui/src/library_ui.rs#L1531), [`ui/dr-ui/src/library_ui.rs:2058`](../ui/dr-ui/src/library_ui.rs#L2058), [`ui/dr-ui/src/library_ui.rs:267`](../ui/dr-ui/src/library_ui.rs#L267), [`ui/dr-ui/src/library_ui.rs:278`](../ui/dr-ui/src/library_ui.rs#L278), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:298`](../ui/dr-ui/src/library_ui.rs#L298), [`ui/dr-ui/src/library_ui.rs:307`](../ui/dr-ui/src/library_ui.rs#L307), [`ui/dr-ui/src/library_ui.rs:399`](../ui/dr-ui/src/library_ui.rs#L399), [`ui/dr-ui/src/library_ui.rs:409`](../ui/dr-ui/src/library_ui.rs#L409), [`ui/dr-ui/src/library_ui.rs:451`](../ui/dr-ui/src/library_ui.rs#L451), [`ui/dr-ui/src/library_ui.rs:514`](../ui/dr-ui/src/library_ui.rs#L514), [`ui/dr-ui/src/library_ui.rs:5239`](../ui/dr-ui/src/library_ui.rs#L5239), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:545`](../ui/dr-ui/src/library_ui.rs#L545), [`ui/dr-ui/src/library_ui.rs:557`](../ui/dr-ui/src/library_ui.rs#L557), [`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:2649`](../ui/dr-ui/ui/app.slint#L2649), [`ui/dr-ui/ui/app.slint:579`](../ui/dr-ui/ui/app.slint#L579), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:369`](../ui/dr-ui/ui/collections.slint#L369), [`ui/dr-ui/ui/collections.slint:52`](../ui/dr-ui/ui/collections.slint#L52), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/collections.slint:84`](../ui/dr-ui/ui/collections.slint#L84), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260), [`ui/dr-ui/ui/library.slint:973`](../ui/dr-ui/ui/library.slint#L973) | | FR-NC-6b | [`ui/dr-ui/src/library_ui.rs:1294`](../ui/dr-ui/src/library_ui.rs#L1294) | | FR-NC-6c | [`core/dr-sync-nextcloud/src/desktop_client.rs:30`](../core/dr-sync-nextcloud/src/desktop_client.rs#L30), [`core/dr-types/src/lib.rs:119`](../core/dr-types/src/lib.rs#L119), [`core/dr-types/src/lib.rs:201`](../core/dr-types/src/lib.rs#L201), [`ui/dr-ui/src/activity.rs:1`](../ui/dr-ui/src/activity.rs#L1), [`ui/dr-ui/src/collections_ui.rs:3195`](../ui/dr-ui/src/collections_ui.rs#L3195), [`ui/dr-ui/src/collections_ui.rs:621`](../ui/dr-ui/src/collections_ui.rs#L621), [`ui/dr-ui/src/collections_ui.rs:683`](../ui/dr-ui/src/collections_ui.rs#L683), [`ui/dr-ui/src/library_ui.rs:1065`](../ui/dr-ui/src/library_ui.rs#L1065), [`ui/dr-ui/src/library_ui.rs:1138`](../ui/dr-ui/src/library_ui.rs#L1138), [`ui/dr-ui/ui/collections.slint:249`](../ui/dr-ui/ui/collections.slint#L249), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682), [`ui/dr-ui/ui/icons.slint:260`](../ui/dr-ui/ui/icons.slint#L260) | | FR-NC-7 | [`core/dr-sync-nextcloud/src/lib.rs:95`](../core/dr-sync-nextcloud/src/lib.rs#L95), [`ui/dr-ui/src/derived_sync.rs:1`](../ui/dr-ui/src/derived_sync.rs#L1), [`ui/dr-ui/src/library.rs:2624`](../ui/dr-ui/src/library.rs#L2624), [`ui/dr-ui/src/library_ui.rs:3608`](../ui/dr-ui/src/library_ui.rs#L3608), [`ui/dr-ui/ui/settings.slint:340`](../ui/dr-ui/ui/settings.slint#L340) | -| 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:116`](../core/dr-types/src/settings.rs#L116), [`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:1002`](../ui/dr-ui/src/lib.rs#L1002), [`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:123`](../ui/dr-ui/src/import.rs#L123), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import.rs:585`](../ui/dr-ui/src/import.rs#L585), [`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:1002`](../ui/dr-ui/src/lib.rs#L1002) | -| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:121`](../core/dr-pipeline/src/sidecar.rs#L121), [`core/dr-pipeline/src/sidecar.rs:91`](../core/dr-pipeline/src/sidecar.rs#L91), [`ui/dr-ui/src/lib.rs:1579`](../ui/dr-ui/src/lib.rs#L1579), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466) | -| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:159`](../core/dr-pipeline/src/sidecar.rs#L159), [`core/dr-pipeline/src/sidecar.rs:186`](../core/dr-pipeline/src/sidecar.rs#L186), [`core/dr-pipeline/src/sidecar.rs:1916`](../core/dr-pipeline/src/sidecar.rs#L1916), [`core/dr-pipeline/src/sidecar.rs:330`](../core/dr-pipeline/src/sidecar.rs#L330), [`core/dr-pipeline/src/sidecar.rs:428`](../core/dr-pipeline/src/sidecar.rs#L428), [`core/dr-pipeline/src/spot.rs:199`](../core/dr-pipeline/src/spot.rs#L199), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:880`](../ui/dr-ui/src/library.rs#L880) | +| 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:116`](../core/dr-types/src/settings.rs#L116), [`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:1008`](../ui/dr-ui/src/lib.rs#L1008), [`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:123`](../ui/dr-ui/src/import.rs#L123), [`ui/dr-ui/src/import.rs:337`](../ui/dr-ui/src/import.rs#L337), [`ui/dr-ui/src/import.rs:585`](../ui/dr-ui/src/import.rs#L585), [`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:1008`](../ui/dr-ui/src/lib.rs#L1008) | +| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:121`](../core/dr-pipeline/src/sidecar.rs#L121), [`core/dr-pipeline/src/sidecar.rs:91`](../core/dr-pipeline/src/sidecar.rs#L91), [`ui/dr-ui/src/lib.rs:1597`](../ui/dr-ui/src/lib.rs#L1597), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466) | +| FR-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:337`](../core/dr-catalog/src/schema.rs#L337), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:159`](../core/dr-pipeline/src/sidecar.rs#L159), [`core/dr-pipeline/src/sidecar.rs:186`](../core/dr-pipeline/src/sidecar.rs#L186), [`core/dr-pipeline/src/sidecar.rs:1916`](../core/dr-pipeline/src/sidecar.rs#L1916), [`core/dr-pipeline/src/sidecar.rs:330`](../core/dr-pipeline/src/sidecar.rs#L330), [`core/dr-pipeline/src/sidecar.rs:428`](../core/dr-pipeline/src/sidecar.rs#L428), [`core/dr-pipeline/src/spot.rs:245`](../core/dr-pipeline/src/spot.rs#L245), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`ui/dr-ui/src/library.rs:750`](../ui/dr-ui/src/library.rs#L750), [`ui/dr-ui/src/library.rs:880`](../ui/dr-ui/src/library.rs#L880) | | FR-PLAT-AND-1 | [`core/dr-types/src/lib.rs:53`](../core/dr-types/src/lib.rs#L53) | | FR-PLAT-AND-3 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) | -| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/lib.rs:789`](../ui/dr-ui/src/lib.rs#L789), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | +| FR-PLAT-LIN-1 | [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/lib.rs:795`](../ui/dr-ui/src/lib.rs#L795), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) | | FR-RAW-1 | [`core/dr-decode/src/lib.rs:243`](../core/dr-decode/src/lib.rs#L243), [`core/dr-types/src/lib.rs:129`](../core/dr-types/src/lib.rs#L129), [`core/dr-types/src/lib.rs:200`](../core/dr-types/src/lib.rs#L200) | | 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:175`](../ui/dr-ui/src/lib.rs#L175) | +| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:176`](../ui/dr-ui/src/lib.rs#L176) | | 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 | [`ui/dr-ui/src/lib.rs:2489`](../ui/dr-ui/src/lib.rs#L2489), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:1031`](../ui/dr-ui/ui/library.slint#L1031) | -| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:67`](../ui/dr-ui/src/lib.rs#L67), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:6345`](../ui/dr-ui/src/library_ui.rs#L6345), [`ui/dr-ui/ui/app.slint:2142`](../ui/dr-ui/ui/app.slint#L2142), [`ui/dr-ui/ui/app.slint:262`](../ui/dr-ui/ui/app.slint#L262), [`ui/dr-ui/ui/app.slint:606`](../ui/dr-ui/ui/app.slint#L606), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/library.slint:1186`](../ui/dr-ui/ui/library.slint#L1186), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:1199`](../ui/dr-ui/ui/library.slint#L1199), [`ui/dr-ui/ui/library.slint:2266`](../ui/dr-ui/ui/library.slint#L2266), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/settings.slint:93`](../ui/dr-ui/ui/settings.slint#L93) | -| FR-UI-3 | [`ui/dr-ui/src/develop.rs:1832`](../ui/dr-ui/src/develop.rs#L1832), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/ui/app.slint:1915`](../ui/dr-ui/ui/app.slint#L1915), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682) | -| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/library_ui.rs:4414`](../ui/dr-ui/src/library_ui.rs#L4414), [`ui/dr-ui/src/library_ui.rs:4517`](../ui/dr-ui/src/library_ui.rs#L4517), [`ui/dr-ui/src/library_ui.rs:4545`](../ui/dr-ui/src/library_ui.rs#L4545), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/ui/app.slint:1611`](../ui/dr-ui/ui/app.slint#L1611), [`ui/dr-ui/ui/app.slint:582`](../ui/dr-ui/ui/app.slint#L582), [`ui/dr-ui/ui/app.slint:613`](../ui/dr-ui/ui/app.slint#L613), [`ui/dr-ui/ui/library.slint:1186`](../ui/dr-ui/ui/library.slint#L1186), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:1199`](../ui/dr-ui/ui/library.slint#L1199), [`ui/dr-ui/ui/library.slint:2266`](../ui/dr-ui/ui/library.slint#L2266), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) | -| 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:2134`](../ui/dr-ui/src/lib.rs#L2134), [`ui/dr-ui/src/lib.rs:2523`](../ui/dr-ui/src/lib.rs#L2523), [`ui/dr-ui/src/lib.rs:2708`](../ui/dr-ui/src/lib.rs#L2708), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:297`](../ui/dr-ui/ui/app.slint#L297), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | +| FR-UI-1 | [`ui/dr-ui/src/lib.rs:2526`](../ui/dr-ui/src/lib.rs#L2526), [`ui/dr-ui/src/lib.rs:68`](../ui/dr-ui/src/lib.rs#L68), [`ui/dr-ui/src/masks_ui.rs:816`](../ui/dr-ui/src/masks_ui.rs#L816), [`ui/dr-ui/ui/library.slint:1031`](../ui/dr-ui/ui/library.slint#L1031) | +| FR-UI-2 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/lib.rs:68`](../ui/dr-ui/src/lib.rs#L68), [`ui/dr-ui/src/library_ui.rs:286`](../ui/dr-ui/src/library_ui.rs#L286), [`ui/dr-ui/src/library_ui.rs:5124`](../ui/dr-ui/src/library_ui.rs#L5124), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/src/library_ui.rs:6345`](../ui/dr-ui/src/library_ui.rs#L6345), [`ui/dr-ui/ui/app.slint:2345`](../ui/dr-ui/ui/app.slint#L2345), [`ui/dr-ui/ui/app.slint:263`](../ui/dr-ui/ui/app.slint#L263), [`ui/dr-ui/ui/app.slint:609`](../ui/dr-ui/ui/app.slint#L609), [`ui/dr-ui/ui/app.slint:616`](../ui/dr-ui/ui/app.slint#L616), [`ui/dr-ui/ui/library.slint:1186`](../ui/dr-ui/ui/library.slint#L1186), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:1199`](../ui/dr-ui/ui/library.slint#L1199), [`ui/dr-ui/ui/library.slint:2266`](../ui/dr-ui/ui/library.slint#L2266), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/settings.slint:93`](../ui/dr-ui/ui/settings.slint#L93) | +| FR-UI-3 | [`ui/dr-ui/src/develop.rs:1842`](../ui/dr-ui/src/develop.rs#L1842), [`ui/dr-ui/src/develop.rs:1949`](../ui/dr-ui/src/develop.rs#L1949), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/masks_ui.rs:218`](../ui/dr-ui/src/masks_ui.rs#L218), [`ui/dr-ui/src/masks_ui.rs:908`](../ui/dr-ui/src/masks_ui.rs#L908), [`ui/dr-ui/src/masks_ui.rs:930`](../ui/dr-ui/src/masks_ui.rs#L930), [`ui/dr-ui/src/spots_ui.rs:19`](../ui/dr-ui/src/spots_ui.rs#L19), [`ui/dr-ui/ui/app.slint:1987`](../ui/dr-ui/ui/app.slint#L1987), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/collections.slint:682`](../ui/dr-ui/ui/collections.slint#L682) | +| FR-UI-4 | [`ui/dr-ui/src/collections_ui.rs:1005`](../ui/dr-ui/src/collections_ui.rs#L1005), [`ui/dr-ui/src/collections_ui.rs:118`](../ui/dr-ui/src/collections_ui.rs#L118), [`ui/dr-ui/src/collections_ui.rs:131`](../ui/dr-ui/src/collections_ui.rs#L131), [`ui/dr-ui/src/collections_ui.rs:1508`](../ui/dr-ui/src/collections_ui.rs#L1508), [`ui/dr-ui/src/collections_ui.rs:1522`](../ui/dr-ui/src/collections_ui.rs#L1522), [`ui/dr-ui/src/collections_ui.rs:1568`](../ui/dr-ui/src/collections_ui.rs#L1568), [`ui/dr-ui/src/collections_ui.rs:159`](../ui/dr-ui/src/collections_ui.rs#L159), [`ui/dr-ui/src/collections_ui.rs:1666`](../ui/dr-ui/src/collections_ui.rs#L1666), [`ui/dr-ui/src/collections_ui.rs:1693`](../ui/dr-ui/src/collections_ui.rs#L1693), [`ui/dr-ui/src/collections_ui.rs:485`](../ui/dr-ui/src/collections_ui.rs#L485), [`ui/dr-ui/src/collections_ui.rs:514`](../ui/dr-ui/src/collections_ui.rs#L514), [`ui/dr-ui/src/collections_ui.rs:582`](../ui/dr-ui/src/collections_ui.rs#L582), [`ui/dr-ui/src/collections_ui.rs:995`](../ui/dr-ui/src/collections_ui.rs#L995), [`ui/dr-ui/src/library_ui.rs:4369`](../ui/dr-ui/src/library_ui.rs#L4369), [`ui/dr-ui/src/library_ui.rs:4414`](../ui/dr-ui/src/library_ui.rs#L4414), [`ui/dr-ui/src/library_ui.rs:4517`](../ui/dr-ui/src/library_ui.rs#L4517), [`ui/dr-ui/src/library_ui.rs:4545`](../ui/dr-ui/src/library_ui.rs#L4545), [`ui/dr-ui/src/library_ui.rs:5257`](../ui/dr-ui/src/library_ui.rs#L5257), [`ui/dr-ui/src/library_ui.rs:5269`](../ui/dr-ui/src/library_ui.rs#L5269), [`ui/dr-ui/ui/app.slint:1659`](../ui/dr-ui/ui/app.slint#L1659), [`ui/dr-ui/ui/app.slint:585`](../ui/dr-ui/ui/app.slint#L585), [`ui/dr-ui/ui/app.slint:616`](../ui/dr-ui/ui/app.slint#L616), [`ui/dr-ui/ui/library.slint:1186`](../ui/dr-ui/ui/library.slint#L1186), [`ui/dr-ui/ui/library.slint:1193`](../ui/dr-ui/ui/library.slint#L1193), [`ui/dr-ui/ui/library.slint:1199`](../ui/dr-ui/ui/library.slint#L1199), [`ui/dr-ui/ui/library.slint:2266`](../ui/dr-ui/ui/library.slint#L2266), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:872`](../ui/dr-ui/ui/library.slint#L872), [`ui/dr-ui/ui/library.slint:891`](../ui/dr-ui/ui/library.slint#L891) | +| 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:2153`](../ui/dr-ui/src/lib.rs#L2153), [`ui/dr-ui/src/lib.rs:2560`](../ui/dr-ui/src/lib.rs#L2560), [`ui/dr-ui/src/lib.rs:2745`](../ui/dr-ui/src/lib.rs#L2745), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:298`](../ui/dr-ui/ui/app.slint#L298), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) | | FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:100`](../core/dr-pipeline/src/descriptor.rs#L100), [`core/dr-pipeline/src/framing.rs:259`](../core/dr-pipeline/src/framing.rs#L259) | | NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1) | -| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1555`](../ui/dr-ui/src/export.rs#L1555), [`ui/dr-ui/src/export.rs:1581`](../ui/dr-ui/src/export.rs#L1581), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:1884`](../ui/dr-ui/src/lib.rs#L1884), [`ui/dr-ui/ui/app.slint:1015`](../ui/dr-ui/ui/app.slint#L1015), [`ui/dr-ui/ui/library.slint:927`](../ui/dr-ui/ui/library.slint#L927) | +| NFR-ARCH-3 | [`ui/dr-ui/src/export.rs:1555`](../ui/dr-ui/src/export.rs#L1555), [`ui/dr-ui/src/export.rs:1581`](../ui/dr-ui/src/export.rs#L1581), [`ui/dr-ui/src/export.rs:410`](../ui/dr-ui/src/export.rs#L410), [`ui/dr-ui/src/export.rs:436`](../ui/dr-ui/src/export.rs#L436), [`ui/dr-ui/src/lib.rs:1902`](../ui/dr-ui/src/lib.rs#L1902), [`ui/dr-ui/ui/app.slint:1063`](../ui/dr-ui/ui/app.slint#L1063), [`ui/dr-ui/ui/library.slint:927`](../ui/dr-ui/ui/library.slint#L927) | | 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), [`ui/dr-ui/src/export.rs:500`](../ui/dr-ui/src/export.rs#L500) | | NFR-OPS-1 | [`tools/traceability/src/lib.rs:266`](../tools/traceability/src/lib.rs#L266) | | 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/traceability/src/lib.rs:479`](../tools/traceability/src/lib.rs#L479) | @@ -119,7 +119,7 @@ _None._ | NFR-R5 | [`core/dr-catalog/src/collections.rs:1`](../core/dr-catalog/src/collections.rs#L1), [`core/dr-catalog/src/error.rs:1`](../core/dr-catalog/src/error.rs#L1), [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/schema.rs:1`](../core/dr-catalog/src/schema.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-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59) | +| NFR-RES-1 | [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`ui/dr-ui/src/lib.rs:60`](../ui/dr-ui/src/lib.rs#L60) | | NFR-RES-4 | [`core/dr-catalog/src/cache.rs:1`](../core/dr-catalog/src/cache.rs#L1), [`core/dr-catalog/src/schema.rs:394`](../core/dr-catalog/src/schema.rs#L394), [`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:2598`](../ui/dr-ui/src/library.rs#L2598) | | NFR-SEC-1 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1) | | R1 | [`tools/traceability/src/lib.rs:495`](../tools/traceability/src/lib.rs#L495), [`tools/traceability/src/lib.rs:499`](../tools/traceability/src/lib.rs#L499) | diff --git a/ui/dr-ui/src/develop.rs b/ui/dr-ui/src/develop.rs index 0efa1c0..5d642b3 100644 --- a/ui/dr-ui/src/develop.rs +++ b/ui/dr-ui/src/develop.rs @@ -533,6 +533,15 @@ pub struct DevelopSession { /// "make these three subjects a stop darker" is one gesture rather than /// three. Order is insertion order and nothing reads it, only membership. active_masks: Vec, + /// TRACES: FR-DEV-8 + /// Which repair the panel is describing, if any. + /// + /// Interface state and not part of the edit, exactly as `active_masks` is: + /// it changes no pixel, it is not in the sidecar, and it is not on the undo + /// stack. One at a time rather than a set — a repair is eight numbers and + /// there is no gesture that usefully moves several at once, where three + /// masked layers really can share a slider drag. + selected_spot: Option, /// Whether to draw the false-coloured region overlay. show_overlay: bool, /// Which attribute the panel is filtered to, or all of them. @@ -619,6 +628,7 @@ impl DevelopSession { subjects: None, subject_key: 0, active_masks: Vec::new(), + selected_spot: None, show_overlay: false, active_tab: None, curve_channel: 0, @@ -1897,6 +1907,235 @@ impl DevelopSession { self.history.record(&self.graph, Edit::Discrete); } + // --- repairs (FR-DEV-8) ------------------------------------------------ + + /// TRACES: FR-DEV-8 + /// Cover what is at `(x, y)`, in fractions of the shown image. + /// + /// The click arrives in *output* coordinates — where the photograph + /// currently sits on screen — and a repair is stored against the + /// photograph, so it goes through `Framing::source_at`: the same map the + /// shader applies, run backwards. Anything less would put the repair where + /// the pointer was rather than where the mark is, and the two agree only at + /// fit-to-window with no crop. + /// + /// Returns the new repair's id, or `None` when the set is full. Selecting + /// it is deliberate: the control that changes its size is in the column, + /// and a photographer who has just placed a spot too small should find that + /// control already pointed at it. + pub fn place_spot(&mut self, x: f32, y: f32) -> Option { + let (sw, sh) = self.demosaiced.size(); + let centre = self.graph.framing().source_at((x, y), sw, sh); + // Outside the photograph entirely — the letterbox margin, or a drag + // that ended off the edge. Placing a repair there would put a disc + // somewhere the user cannot see and cannot pick up again. + if !(0.0..=1.0).contains(¢re.0) || !(0.0..=1.0).contains(¢re.1) { + return None; + } + + let aspect = sw.max(1) as f32 / sh.max(1) as f32; + let radius = dr_pipeline::spot::DEFAULT_RADIUS; + let offset = dr_pipeline::Spot::default_offset(centre, radius, aspect); + + let id = self + .graph + .spots_mut() + .place(dr_pipeline::Spot::new(centre, offset, radius))?; + self.selected_spot = Some(id.clone()); + self.history.record(&self.graph, Edit::Discrete); + Some(id) + } + + /// TRACES: FR-DEV-8 | FR-UI-3 + /// Every repair as a circle on the shown image, plus the source circle of + /// the selected one. + /// + /// # Why only the selected repair shows its source + /// + /// A dusty sky carries a dozen repairs. Two dozen circles with nothing + /// saying which source belongs to which disc is not more information, it is + /// less — and there is no room on a phone for a connector between each + /// pair. The selection is what disambiguates them, which is also why a + /// press on a repair selects it before the drag begins. + /// + /// Recomputed per redraw rather than cached, for the reason + /// [`Self::gradient_handles`] gives: the answer changes with the *view*, + /// and a pan moves every circle while touching no edit. + pub fn spot_handles(&self) -> Vec { + let (sw, sh) = self.demosaiced.size(); + let framing = self.graph.framing(); + let aspect = sw.max(1) as f32 / sh.max(1) as f32; + // The shown image's own shape, which is not the source's once the frame + // has been cropped or turned. A radius is reported against its height, + // so this is what converts the x half of the mapped offset. + let (ow, oh) = self.graph.output_size(sw, sh); + let shown_aspect = ow.max(1) as f32 / oh.max(1) as f32; + + let mut handles = Vec::new(); + for spot in self.graph.spots().spots() { + let selected = self.selected_spot.as_deref() == Some(spot.id.as_str()); + let centre = framing.output_at(spot.centre, sw, sh); + + // The radius, mapped rather than scaled: a point one radius above + // the centre goes through the same map, and the distance between + // the two answers is the radius as drawn. The x half is multiplied + // by the shown aspect because the two axes are normalised by + // different lengths, and a circle measured in mixed units is an + // ellipse. + let rim = framing.output_at((spot.centre.0, spot.centre.1 + spot.radius), sw, sh); + let radius = ((rim.0 - centre.0) * shown_aspect).hypot(rim.1 - centre.1); + + handles.push(crate::SpotHandle { + id: spot.id.clone().into(), + role: crate::SpotRole::Destination, + x: centre.0, + y: centre.1, + radius, + selected, + enabled: spot.enabled, + }); + + if selected { + let source = framing.output_at(spot.source(aspect), sw, sh); + handles.push(crate::SpotHandle { + id: spot.id.clone().into(), + role: crate::SpotRole::Source, + x: source.0, + y: source.1, + radius, + selected: true, + enabled: spot.enabled, + }); + } + } + handles + } + + /// TRACES: FR-DEV-8 + /// Drag one circle of one repair, from `press` to `now`, both in fractions + /// of the shown image. + /// + /// Dragging the disc moves the whole repair and carries its source along — + /// what a photographer means by nudging a spot. Dragging the source moves + /// the source alone, which is the override FR-DEV-8 asks for over the + /// automatic placement. + /// + /// `origin` is the repair as it stood when the gesture began; the caller + /// holds it for the duration and hands it back, so a drag is applied to + /// that rather than accumulated frame by frame — the rule + /// [`Self::drag_gradient_handle`] states, for the same reasons. + pub fn drag_spot( + &mut self, + id: &str, + role: crate::SpotRole, + origin: Option<&dr_pipeline::Spot>, + press: (f32, f32), + now: (f32, f32), + ) -> Option { + let (sw, sh) = self.demosaiced.size(); + let framing = *self.graph.framing(); + let aspect = sw.max(1) as f32 / sh.max(1) as f32; + + let start = match origin { + Some(spot) => spot.clone(), + None => self.graph.spots().get(id)?.clone(), + }; + + // The displacement in source coordinates. Affine, so a movement is a + // movement: the map may be run on the two endpoints and subtracted, + // which is what makes a drag on a rotated photograph move the repair in + // the direction the finger went. + let from = framing.source_at(press, sw, sh); + let to = framing.source_at(now, sw, sh); + let moved = (to.0 - from.0, to.1 - from.1); + + let spot = self.graph.spots_mut().get_mut(id)?; + match role { + crate::SpotRole::Destination => { + spot.set_centre((start.centre.0 + moved.0, start.centre.1 + moved.1)); + } + // In frame units, because that is what an offset is stored in — and + // the x half of a normalised displacement is short by the aspect. + crate::SpotRole::Source => { + spot.set_offset((start.offset.0 + moved.0 * aspect, start.offset.1 + moved.1)); + } + } + // Nothing recorded here: a drag delivers a pointer event a frame, and + // one history step apiece would make undo walk the gesture back pixel + // by pixel. Recorded once, on release. + Some(start) + } + + /// A repair's drag finished: one history step for the whole gesture. + pub fn commit_spot_drag(&mut self) { + self.history.record(&self.graph, Edit::Discrete); + } + + /// Which repair the column is describing. + pub fn selected_spot(&self) -> Option<&dr_pipeline::Spot> { + let id = self.selected_spot.as_deref()?; + self.graph.spots().get(id) + } + + pub fn selected_spot_id(&self) -> Option<&str> { + self.selected_spot.as_deref() + } + + /// Choose a repair, or `None` to describe none. + /// + /// An id the graph no longer holds selects nothing rather than being kept: + /// the circle that offered it is stale by the time the press lands, and a + /// selection pointing at a deleted repair would leave the column describing + /// something that is not on the photograph. + pub fn select_spot(&mut self, id: Option<&str>) { + self.selected_spot = id + .filter(|id| self.graph.spots().get(id).is_some()) + .map(str::to_string); + } + + /// TRACES: FR-DEV-8 + /// Take a repair off the photograph, returning whether one went. + pub fn remove_spot(&mut self, id: &str) -> bool { + if self.graph.spots_mut().remove(id).is_none() { + return false; + } + if self.selected_spot.as_deref() == Some(id) { + self.selected_spot = None; + } + self.history.record(&self.graph, Edit::Discrete); + true + } + + /// TRACES: FR-DEV-8 + /// Change one of the selected repair's settings. + /// + /// Coalesced under [`Edit::Discrete`] like the drags are, rather than under + /// a parameter key: a repair is not an operation and has no `OpId` to + /// coalesce by, so a slider drag over it records a step per movement unless + /// the caller debounces. `SliderRow` fires once per completed gesture, + /// which is what makes that acceptable here and is why this is the one + /// panel in the application built from that row rather than from a live + /// track. + pub fn set_selected_spot(&mut self, change: F) -> bool + where + F: FnOnce(&mut dr_pipeline::Spot), + { + let Some(id) = self.selected_spot.clone() else { + return false; + }; + let Some(spot) = self.graph.spots_mut().get_mut(&id) else { + return false; + }; + change(spot); + self.history.record(&self.graph, Edit::Discrete); + true + } + + /// How many repairs this photograph carries. + pub fn spot_count(&self) -> usize { + self.graph.spots().len() + } + /// The selected layer's mask rule, for a caller that has to remember what /// a gesture started from. pub fn active_mask_source(&self) -> Option { diff --git a/ui/dr-ui/src/lib.rs b/ui/dr-ui/src/lib.rs index d0f9ebe..f180a5d 100644 --- a/ui/dr-ui/src/lib.rs +++ b/ui/dr-ui/src/lib.rs @@ -40,6 +40,7 @@ mod segmentation; mod settings_store; mod settings_ui; mod sidecar_cache; +mod spots_ui; mod trash; use std::cell::RefCell; @@ -292,6 +293,11 @@ fn reset_view_state(window: &AppWindow) { // overlay or the crosshair to the next one would offer a selection of // regions that are not in the picture on screen. masks_ui::reset(window); + // TRACES: FR-DEV-8 + // The repairs belong to one photograph too. The next one's arrive with its + // sidecar a moment later, and until they do the canvas must not be showing + // the last one's. + spots_ui::reset(window); } /// Push the framing back to the geometry panel. @@ -1279,6 +1285,18 @@ pub fn run(paths: Vec) -> Result<()> { // those ends in a redraw. masks_ui::sync_overlay_view(window, s); + // TRACES: FR-DEV-8 + // And where the repairs are drawn, for the same reason: a pan or a + // zoom moves every circle while touching no repair, and a circle + // left where the mark used to be is worse than no circle at all. + spots_ui::sync_handles(window, s); + // The column's own numbers, pushed from here as well so that a + // photograph opened with repairs already on it arrives with the + // panel describing them — the sidecar lands after the callbacks + // have all been installed, and a redraw is the one path every + // arrival takes. + spots_ui::sync_panel(window, s); + let (mut w, mut h) = *viewport.borrow(); // **Half resolution while the gesture is still moving.** @@ -1951,6 +1969,7 @@ pub fn run(paths: Vec) -> Result<()> { } masks_ui::wire(&window, &session, &rows, &redraw); + spots_ui::wire(&window, &session, redraw.clone()); { let weak = window.as_weak(); @@ -2166,14 +2185,32 @@ pub fn run(paths: Vec) -> Result<()> { w.set_crop_h(c.height); } ViewMode::Local => s.set_overlay(true), + // TRACES: FR-DEV-8 + // Nothing to arm: the circles are drawn whenever there are + // repairs, and what the mode changes is whether a click on + // the photograph makes another one. Leaving the mask + // selection behind would re-point the column at a layer's + // chain while the canvas is showing repairs, which is the + // fault this whole strip exists to prevent. + ViewMode::Spots => { + s.set_overlay(false); + s.set_active_mask(None); + } ViewMode::Photo => { s.set_overlay(false); s.set_active_mask(None); + // A repair stays on the photograph; only the *selection* + // goes, so the source circle does not hang about over a + // frame nobody is repairing any more. + s.select_spot(None); } } } w.set_view_mode(mode); masks_ui::sync(&w, &session); + if let Some(s) = session.borrow().as_ref() { + spots_ui::sync_handles(&w, s); + } // The scope may have just changed, so the panel below is now // describing a different chain. sync_rows(&w, &rows, &session); diff --git a/ui/dr-ui/src/spots_ui.rs b/ui/dr-ui/src/spots_ui.rs new file mode 100644 index 0000000..61bdbd9 --- /dev/null +++ b/ui/dr-ui/src/spots_ui.rs @@ -0,0 +1,292 @@ +//! TRACES: FR-DEV-8 +//! Wiring the repair tool to the develop session. +//! +//! Translation only, exactly as [`crate::masks_ui`] is: circles out in one +//! direction, gestures in the other, and every decision in +//! [`crate::develop::DevelopSession`]. What this module does decide is when the +//! canvas is redrawn and when the panel is rebuilt, and those are the two +//! things that go wrong quietly here — see [`sync_handles`] for the one that +//! took a gesture out from under the finger. + +use std::cell::RefCell; +use std::rc::Rc; + +use slint::{ComponentHandle as _, Model as _, ModelRc, VecModel}; + +use crate::develop::DevelopSession; +use crate::{AppWindow, SpotHandle}; + +/// TRACES: FR-DEV-8 | FR-UI-3 +/// Move the circles to where the repairs now are. +/// +/// # Why this is not `set_spot_handles(VecModel::from(…))` +/// +/// **A fresh model kills the gesture that is moving them.** The circles are a +/// repeater over this model, and handing Slint a new `ModelRc` makes it throw +/// the repeated items away and build new ones — the `TouchArea` holding the +/// pointer included. The drag then dies under the finger with the button still +/// down. `masks_ui::sync_handles` carries the same warning, and `develop.rs` +/// carries it about the parameter rows, where it broke slider drags: this is +/// the third place the same mistake is available, which is why it is written +/// down in all three. +/// +/// So the model is kept and its rows are rewritten in place. +pub(crate) fn sync_handles(window: &AppWindow, session: &DevelopSession) { + let next = session.spot_handles(); + let model = handle_model(); + + while model.row_count() > next.len() { + model.remove(model.row_count() - 1); + } + for (i, handle) in next.into_iter().enumerate() { + if i < model.row_count() { + // Only where it actually moved: an unchanged row written back is + // still a change notification. + if model.row_data(i).as_ref() != Some(&handle) { + model.set_row_data(i, handle); + } + } else { + model.push(handle); + } + } + + window.set_spot_handles(ModelRc::from(model)); + window.set_selected_spot(session.selected_spot_id().unwrap_or_default().into()); +} + +/// The circles' model, held for the life of the process — one shared identity, +/// for the reason [`sync_handles`] gives. +fn handle_model() -> Rc> { + thread_local! { + static HANDLES: Rc> = Rc::new(VecModel::default()); + } + HANDLES.with(Clone::clone) +} + +/// Take the repairs off the canvas between photographs. +/// +/// Emptied rather than replaced, keeping the model's identity for the reason +/// [`sync_handles`] gives. +pub(crate) fn reset(window: &AppWindow) { + let model = handle_model(); + while model.row_count() > 0 { + model.remove(model.row_count() - 1); + } + window.set_spot_handles(ModelRc::from(model)); + window.set_selected_spot(Default::default()); + window.set_spot_count(0); +} + +/// Install the repair tool's callbacks. +pub(crate) fn wire( + window: &AppWindow, + session: &Rc>>, + redraw: Rc, +) { + // --- placing ---------------------------------------------------------- + { + let weak = window.as_weak(); + let session = session.clone(); + let redraw = redraw.clone(); + window.on_spot_placed(move |x, y| { + let Some(w) = weak.upgrade() else { return }; + let placed = session + .borrow_mut() + .as_mut() + .and_then(|s| s.place_spot(x, y)); + if placed.is_none() { + // The letterbox margin, or a full set. Neither is an error and + // neither should clear the selection: a near-miss that threw + // away what the column was describing would read as hostile. + return; + } + if let Some(s) = session.borrow().as_ref() { + sync_handles(&w, s); + sync_panel(&w, s); + } + sync_undo(&w, &session); + redraw(&w); + }); + } + + // --- dragging --------------------------------------------------------- + // + // The repair as it stood when the press landed, held for the gesture. A + // drag is applied to *that* rather than accumulated frame by frame: the + // clamps would otherwise compound, so a source dragged past its limit and + // back would not return to where it started, and the result would depend on + // how many pointer events the platform happened to deliver. + let dragging: Rc>> = Rc::new(RefCell::new(None)); + { + let weak = window.as_weak(); + let session = session.clone(); + let redraw = redraw.clone(); + let dragging = dragging.clone(); + window.on_spot_handle_dragged(move |id, role, from_x, from_y, to_x, to_y| { + let Some(w) = weak.upgrade() else { return }; + let origin = dragging.borrow().clone(); + let started = session.borrow_mut().as_mut().and_then(|s| { + s.drag_spot( + id.as_str(), + role, + origin.as_ref(), + (from_x, from_y), + (to_x, to_y), + ) + }); + if started.is_none() { + return; + } + *dragging.borrow_mut() = started; + // Only the circles. Rebuilding the panel on every frame of a drag + // is work for no difference, and a full rebuild would take the + // gesture out from under the finger — see `sync_handles`. + if let Some(s) = session.borrow().as_ref() { + sync_handles(&w, s); + } + redraw(&w); + }); + } + { + let weak = window.as_weak(); + let session = session.clone(); + let dragging = dragging.clone(); + window.on_spot_handle_released(move || { + // Forgotten on release, so the next gesture measures from wherever + // this one left the repair. + if dragging.borrow_mut().take().is_none() { + // A press with no movement — a selection, not a drag. Recording + // a step would put an identical snapshot on the undo stack. + return; + } + if let Some(s) = session.borrow_mut().as_mut() { + s.commit_spot_drag(); + } + if let Some(w) = weak.upgrade() { + sync_undo(&w, &session); + } + }); + } + + // --- the column's controls -------------------------------------------- + // + // One closure shape, four callbacks: each hands the session a change to + // make to whichever repair is selected, and the session decides whether + // there is one. Reading the selection here instead would put the same + // `Option` check in four places and let them disagree. + macro_rules! control { + ($install:ident, $apply:expr) => {{ + let weak = window.as_weak(); + let session = session.clone(); + let redraw = redraw.clone(); + window.$install(move |value| { + let Some(w) = weak.upgrade() else { return }; + let changed = session + .borrow_mut() + .as_mut() + .is_some_and(|s| s.set_selected_spot(|spot| $apply(spot, value))); + if !changed { + return; + } + if let Some(s) = session.borrow().as_ref() { + sync_handles(&w, s); + sync_panel(&w, s); + } + sync_undo(&w, &session); + redraw(&w); + }); + }}; + } + + control!(on_spot_radius_changed, |spot: &mut dr_pipeline::Spot, v| { + spot.set_radius(v) + }); + control!( + on_spot_feather_changed, + |spot: &mut dr_pipeline::Spot, v| { spot.set_feather(v) } + ); + control!( + on_spot_opacity_changed, + |spot: &mut dr_pipeline::Spot, v| { spot.set_opacity(v) } + ); + control!(on_spot_mode_picked, |spot: &mut dr_pipeline::Spot, i| { + spot.mode = if i == 1 { + dr_pipeline::SpotMode::Clone + } else { + dr_pipeline::SpotMode::Heal + } + }); + + // --- selecting and removing ------------------------------------------- + { + let weak = window.as_weak(); + let session = session.clone(); + let redraw = redraw.clone(); + window.on_spot_selected(move |id| { + let Some(w) = weak.upgrade() else { return }; + if let Some(s) = session.borrow_mut().as_mut() { + s.select_spot(Some(id.as_str())); + } + if let Some(s) = session.borrow().as_ref() { + sync_handles(&w, s); + sync_panel(&w, s); + } + // The canvas changes — the selected repair gains its source circle + // — but no pixel does, so this is a repaint of the overlay rather + // than of the photograph. `redraw` is the only hook there is, and + // it is cheap enough: the render caches on the invalidation key, + // which a selection does not move. + redraw(&w); + }); + } + { + let weak = window.as_weak(); + let session = session.clone(); + let redraw = redraw.clone(); + window.on_spot_removed(move |id| { + let Some(w) = weak.upgrade() else { return }; + let removed = session + .borrow_mut() + .as_mut() + .is_some_and(|s| s.remove_spot(id.as_str())); + if !removed { + return; + } + if let Some(s) = session.borrow().as_ref() { + sync_handles(&w, s); + sync_panel(&w, s); + } + sync_undo(&w, &session); + redraw(&w); + }); + } +} + +/// TRACES: FR-DEV-8 +/// Push the selected repair's settings into the column. +/// +/// Separate from [`sync_handles`] because they answer different questions and +/// change at different times: the circles move on every frame of a drag and on +/// every pan, and these move when a repair is selected or a control is used. +pub(crate) fn sync_panel(window: &AppWindow, session: &DevelopSession) { + window.set_spot_count(session.spot_count() as i32); + if let Some(spot) = session.selected_spot() { + window.set_spot_radius(spot.radius); + window.set_spot_feather(spot.feather); + window.set_spot_opacity(spot.opacity); + window.set_spot_mode(match spot.mode { + dr_pipeline::SpotMode::Heal => 0, + dr_pipeline::SpotMode::Clone => 1, + }); + } +} + +/// Push whether undo and redo have anywhere to go. +/// +/// Every repair is a history step, so every one of them moves these — and a +/// disabled undo button after an edit that *is* undoable is the kind of small +/// lie that stops people trusting the button at all. +fn sync_undo(window: &AppWindow, session: &Rc>>) { + window.set_can_undo(session.borrow().as_ref().is_some_and(|s| s.can_undo())); + window.set_can_redo(session.borrow().as_ref().is_some_and(|s| s.can_redo())); +} diff --git a/ui/dr-ui/ui/adjust.slint b/ui/dr-ui/ui/adjust.slint index a831dc7..95966e5 100644 --- a/ui/dr-ui/ui/adjust.slint +++ b/ui/dr-ui/ui/adjust.slint @@ -673,6 +673,14 @@ export enum ViewMode { /// The region map is drawn, a click on the photograph selects, and the /// column is the mask stack and the selected layer's adjustments. local, + /// TRACES: FR-DEV-8 + /// The repairs are drawn on the photograph, a click places one, and the + /// column describes whichever is selected. + /// + /// A mode rather than a panel button for the reason the enum exists at + /// all: arming a click on the canvas is something only one tool may be + /// doing at a time, and two flags could both be true. + spots, } /// The strip: what the photographer is working on. @@ -741,15 +749,17 @@ export component ModeStrip inherits Rectangle { // than as an underline, so at a glance the strip reads as two runs // and it is never ambiguous which half a lit entry belongs to. // - // **Where a brush goes.** Painting a mask is a third mode of exactly - // this shape — it arms a canvas gesture and scopes the column — so it - // joins this list and the `ViewMode` enum, and needs nothing else here. - // `MaskSource::Brush` and the stroke calls on `MaskLayer` land in the - // core separately; what is missing on this side is only the canvas - // interaction, which is the gradient handles' neighbour. + // **Where a canvas tool goes.** A mode that arms a gesture on the + // photograph and scopes the column joins this list and the `ViewMode` + // enum, and needs nothing else here. *Repair* is the first to arrive + // that way (FR-DEV-8); painting a mask is the same shape and still to + // come — `MaskSource::Brush` and the stroke calls on `MaskLayer` + // already exist in the core, and what is missing on this side is only + // the canvas interaction the repairs now have a pattern for. for entry in [ { label: "Crop", value: ViewMode.crop }, { label: "Local", value: ViewMode.local }, + { label: "Repair", value: ViewMode.spots }, ]: mode-chip := TouchArea { width: mode-name.preferred-width + 2 * Theme.gap-sm; height: Theme.touch-target; diff --git a/ui/dr-ui/ui/app.slint b/ui/dr-ui/ui/app.slint index 328670a..ff71a55 100644 --- a/ui/dr-ui/ui/app.slint +++ b/ui/dr-ui/ui/app.slint @@ -1,6 +1,7 @@ import { Theme } from "theme.slint"; import { AdjustPanel, GeometryPanel, ModeStrip, ParamRow, TransferPanel, ViewMode } from "adjust.slint"; import { GradientHandle, HandleRole, MaskPanel, MaskRow, SubjectRow } from "masks.slint"; +import { SpotHandle, SpotPanel, SpotRole } from "spots.slint"; import { LaunchScreen } from "launch.slint"; import { LibraryGrid, LibraryCell, TimelineBar, PhotoRoll, KeywordRow } from "library.slint"; import { Button, PanelHeading, Label, Value, Caption, Panel, EmptyState, ProgressBar, ActivityRow } from "widgets.slint"; @@ -10,7 +11,7 @@ import { SettingsPage } from "settings.slint"; import { ImportPage } from "import.slint"; export { LibraryCell, TimelineBar, CollectionRow, ActivityRow, HistogramView } -export { ViewMode, GradientHandle, HandleRole } +export { ViewMode, GradientHandle, HandleRole, SpotHandle, SpotRole } // Status strip — surfaces the GPU backend and adapter, which matters during // v0.1 because assumption A1 is exactly "does this compositing path work on @@ -313,6 +314,8 @@ export component AppWindow inherits Window { /// comparison is written once rather than at each of a dozen call sites. property cropping: root.view-mode == ViewMode.crop; property local-mode: root.view-mode == ViewMode.local; + /// TRACES: FR-DEV-8 + property repairing: root.view-mode == ViewMode.spots; /// The crop rect in fractions of the frame, mirrored from Rust so the /// overlay draws exactly what the pipeline holds. @@ -969,6 +972,51 @@ export component AppWindow inherits Window { /// rather than as one per frame of the gesture. callback gradient-handle-released(); + // --- repairs (FR-DEV-8) ------------------------------------------------- + + /// Every repair on the photograph, as circles in fractions of the shown + /// image. Rust maps them through the framing, so they follow a crop, a + /// zoom, a pan and a rotation without anything here knowing about any of + /// those. + in property <[SpotHandle]> spot-handles; + /// A click on the photograph in repair mode, in fractions of the shown + /// image: cover what is here. + callback spot-placed(float, float); + /// A repair's circle dragged: which repair, which half of it, where the + /// press landed and where the pointer is now. + /// + /// The press is carried rather than a running delta for the reason the + /// gradient handles carry it — the geometry is re-derived from where it + /// stood when the gesture began, so a drag cannot accumulate rounding + /// error along its length. + callback spot-handle-dragged(string, SpotRole, float, float, float, float); + /// A drag finished, so it can be one history step rather than one a frame. + callback spot-handle-released(); + /// A repair chosen, so the column describes it. + callback spot-selected(string); + /// Take the selected repair off the photograph. + callback spot-removed(string); + /// Which repair the column is describing, or "" for none. + in-out property selected-spot: ""; + /// The selected repair's settings, and how many repairs there are. + /// + /// Pushed as scalars rather than read off `spot-handles`, because the + /// circles carry what is *drawn* — a radius already mapped through the + /// framing into fractions of the shown image — and the panel edits what is + /// *stored*. Deriving one from the other would mean a slider that moved + /// differently at different zoom levels. + in property spot-count: 0; + in property spot-radius: 0.012; + in property spot-feather: 0.35; + in property spot-opacity: 1.0; + /// 0 heal, 1 clone. + in property spot-mode: 0; + + callback spot-radius-changed(float); + callback spot-feather-changed(float); + callback spot-opacity-changed(float); + callback spot-mode-picked(int); + callback segment-image(bool); /// A click on the photograph, in fractions of the shown image, plus /// whether it should extend the selection rather than replace it. @@ -1671,6 +1719,30 @@ in property panel-visible: true; } } + // TRACES: FR-DEV-8 + // Placing a repair, beside the region picker and for the + // same reasons: a click and a pan want different handlers, + // and this one has to reach the click first. + // + // Below the circles declared further down, so a press that + // lands on an existing repair takes hold of it instead of + // making another one on top. + if root.repairing && root.total > 0 && root.load-error == "": TouchArea { + x: parent.shown-x; + y: parent.shown-y; + width: parent.shown-w; + height: parent.shown-h; + mouse-cursor: MouseCursor.crosshair; + enabled: !root.cropping; + + clicked => { + root.spot-placed( + self.mouse-x / max(self.width, 1px), + self.mouse-y / max(self.height, 1px), + ); + } + } + if root.total > 0 && root.load-error == "": TouchArea { x: 0; y: 0; width: 100%; @@ -2010,6 +2082,123 @@ in property panel-visible: true; } } + // --- repairs (FR-DEV-8) ------------------------------------- + // + // **Drawn at the size they are.** A gradient's handles are + // dots because a gradient has no edge to show; a repair is + // a disc, and whether that disc covers a speck of dust is + // the entire judgement a photographer is making. A + // fixed-size dot standing in for it would say nothing about + // the edit. + // + // The source circle appears for the **selected** repair + // only. Every repair showing both would double the circles + // on a dusty sky and leave no way to tell which source + // belongs to which disc; Rust decides, and simply does not + // send the others. + // + // Positioned against `shown-*` like everything else that + // has to land on the picture, and mapped through the + // framing on the Rust side, so a repair follows a crop, a + // zoom and a rotation rather than sitting where it used to + // be. + for spot in root.spot-handles: Rectangle { + property disc: max(2 * spot.radius * parent.shown-h, 8px); + property is-source: spot.role == SpotRole.source; + + x: parent.shown-x + spot.x * parent.shown-w - self.width / 2; + y: parent.shown-y + spot.y * parent.shown-h - self.height / 2; + // Never smaller than a finger, whatever the repair's + // own size: a spot on a dust speck is a few pixels + // across at fit-to-window, and a target that small + // cannot be picked up again on a phone (FR-UI-3). The + // *drawn* circle keeps its true size; only the reach + // is padded. + width: max(self.disc, Theme.touch-target); + height: self.width; + + Rectangle { + width: parent.disc; + height: self.width; + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + border-radius: self.width / 2; + border-width: spot.selected ? 2px : 1px; + // White on a dark ring, so a repair is visible on a + // blown sky and on a black frame alike — the same + // treatment the gradient handles use, and no theme + // token, because this is drawn over the photograph + // rather than over the interface. + border-color: spot.enabled ? #ffffffcc : #ffffff55; + background: parent.is-source + ? #ffffff14 + : (spot.selected ? #ffffff22 : transparent); + } + + // A second, darker ring just outside the first. One + // white circle vanishes against a white sky however it + // is drawn; two rings of opposite tone cannot both + // vanish against anything. + Rectangle { + width: parent.disc + 2px; + height: self.width; + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + border-radius: self.width / 2; + border-width: 1px; + border-color: #00000066; + } + + grab := TouchArea { + width: 100%; + height: 100%; + mouse-cursor: MouseCursor.move; + + // Where the press landed, in the fractions the + // callback reports in — captured on the way down so + // the gesture is measured from one origin. + property from-x; + property from-y; + + function fraction-x(local-x: length) -> float { + return (parent.x + local-x - canvas-area.shown-x) + / max(canvas-area.shown-w, 1px); + } + function fraction-y(local-y: length) -> float { + return (parent.y + local-y - canvas-area.shown-y) + / max(canvas-area.shown-h, 1px); + } + + pointer-event(ev) => { + if (ev.kind == PointerEventKind.down) { + self.from-x = self.fraction-x(self.pressed-x); + self.from-y = self.fraction-y(self.pressed-y); + // Touching a repair is choosing it. On the + // way *down*, so the column has re-scoped + // by the time the drag begins and the + // controls describe what is moving. + root.spot-selected(spot.id); + } + if (ev.kind == PointerEventKind.up) { + root.spot-handle-released(); + } + } + + moved => { + if (self.pressed) { + root.spot-handle-dragged( + spot.id, + spot.role, + self.from-x, + self.from-y, + self.fraction-x(self.mouse-x), + self.fraction-y(self.mouse-y), + ); + } + } + } + } + // Arrow keys and space step through the folder. // // Focused on show rather than waiting for a click, exactly @@ -2047,6 +2236,18 @@ in property panel-visible: true; } return accept; } + // TRACES: FR-DEV-8 + // Delete removes the selected repair, and only in + // repair mode: the same key in the grid judges + // photographs, and a key that means two things + // depending on a mode the user has forgotten they + // are in is how work disappears. + if (root.repairing && root.selected-spot != "" + && (event.text == Key.Delete + || event.text == Key.Backspace)) { + root.spot-removed(root.selected-spot); + return accept; + } if (event.text == Key.RightArrow || event.text == " ") { root.next-image(); return accept; @@ -2089,7 +2290,9 @@ in property panel-visible: true; // Names the mode being left rather than saying // "Done", which was unambiguous while there was // one mode and would not be with two. - text: root.cropping ? "Done Cropping" : "Done Masking"; + text: root.cropping + ? "Done Cropping" + : (root.repairing ? "Done Repairing" : "Done Masking"); active: true; clicked => { root.mode-picked(ViewMode.photo); } } @@ -2214,13 +2417,13 @@ in property panel-visible: true; // screen. `masks.slint` carries the same note for // the same reason. column := VerticalLayout { - if !root.local-mode: InfoPanel { + if !root.local-mode && !root.repairing: InfoPanel { camera: root.camera; exposure: root.exposure; dimensions: root.dimensions; } - if !root.local-mode: Rectangle { + if !root.local-mode && !root.repairing: Rectangle { height: 1px; background: Theme.rule; } @@ -2250,7 +2453,7 @@ in property panel-visible: true; // made rather than how it is applied: the frame is decided // by eye first and the pipeline runs it last (see // `dr_pipeline::framing` on the coordinate order). - if !root.local-mode: GeometryPanel { + if !root.local-mode && !root.repairing: GeometryPanel { enabled: root.adjust-enabled; angle: root.straighten; max-straighten: root.max-straighten; @@ -2266,7 +2469,7 @@ in property panel-visible: true; reset => { root.framing-reset(); } } - if !root.local-mode: Rectangle { + if !root.local-mode && !root.repairing: Rectangle { height: 1px; background: Theme.rule; } @@ -2276,7 +2479,7 @@ in property panel-visible: true; // burying it under thirty sliders would put the one // control that operates on all of them below all of // them. - if !root.local-mode: TransferPanel { + if !root.local-mode && !root.repairing: TransferPanel { enabled: root.adjust-enabled; armed: root.settings-armed; summary: root.settings-summary; @@ -2299,6 +2502,32 @@ in property panel-visible: true; // peers that silently re-points another panel, it is // what that mode's column *is*. That also takes one // panel out of a scrolling column that had six. + // TRACES: FR-DEV-8 + // What the column *is* in repair mode, on the same + // terms as the mask panel: not a panel among peers + // that quietly re-points the sliders below it, but + // the whole of what this mode has to say. + if root.repairing: SpotPanel { + enabled: root.adjust-enabled; + has-selection: root.selected-spot != ""; + count: root.spot-count; + radius: root.spot-radius; + feather: root.spot-feather; + spot-opacity: root.spot-opacity; + mode: root.spot-mode; + + radius-changed(v) => { root.spot-radius-changed(v); } + feather-changed(v) => { root.spot-feather-changed(v); } + opacity-changed(v) => { root.spot-opacity-changed(v); } + mode-picked(i) => { root.spot-mode-picked(i); } + removed => { root.spot-removed(root.selected-spot); } + } + + if root.repairing: Rectangle { + height: 1px; + background: Theme.rule; + } + if root.local-mode: MaskPanel { enabled: root.adjust-enabled; masks: root.mask-rows; diff --git a/ui/dr-ui/ui/spots.slint b/ui/dr-ui/ui/spots.slint new file mode 100644 index 0000000..1e58738 --- /dev/null +++ b/ui/dr-ui/ui/spots.slint @@ -0,0 +1,170 @@ +import { Theme } from "theme.slint"; +import { Button, PanelHeading, Caption, Value } from "widgets.slint"; +import { Segmented, SliderRow } from "controls.slint"; + +// TRACES: FR-DEV-8 +// Spot removal on the canvas: what is drawn over the photograph, and what a +// finger can take hold of. +// +// A repair is two circles and the line between them — the disc being covered, +// and the patch it is copied from. Both are drawn at the size they actually +// are, not as abstract handles, because the size *is* the edit: a photographer +// judging whether a disc covers a mark is judging a circle against a speck, and +// a fixed-size dot standing in for it would tell them nothing. +// +// Positions arrive already mapped through the framing, as fractions of the +// shown image, exactly as `GradientHandle` does — see `spots_ui.rs`. Nothing +// here knows what a crop is. + +/// Which half of a repair a handle is. +export enum SpotRole { + /// The disc over the mark. Dragging it moves the whole repair, source and + /// all, which is what a photographer means by nudging a spot. + destination, + /// Where the patch is read from. Dragging it moves the source alone. + source, +} + +/// One circle of one repair, in fractions of the shown image. +export struct SpotHandle { + id: string, + role: SpotRole, + /// Centre, in fractions of the shown image's width and height. + x: float, + y: float, + /// The disc's radius as a fraction of the shown image's **height**. + /// + /// One axis rather than two, because a repair is a circle: normalising x + /// by the width and y by the height would draw it as an ellipse on every + /// frame that is not square. The caller resolves the aspect on the way in. + radius: float, + /// Whether this repair is the one the panel is describing. + selected: bool, + /// Whether the repair draws at all — a spot switched off is still shown, + /// faintly, because it is still an edit somebody made. + enabled: bool, +} + +/// TRACES: FR-DEV-8 +/// The column while the repair tool is up. +/// +/// One repair at a time, because that is how repairs are made: a photographer +/// covers a mark, looks at it, and moves on. There is no stack here for the +/// same reason there is one for masks — a mask is a thing you come back to and +/// re-shape, and a repair is either right or deleted. +/// +/// **The controls describe the selected repair, and when none is selected they +/// describe nothing.** An earlier shape had them set the defaults for the +/// *next* repair, which reads identically on screen and does something +/// completely different: a photographer dragging Size with nothing selected +/// would see no change on the photograph and conclude the slider was broken. +export component SpotPanel inherits Rectangle { + in property enabled: true; + /// Whether a repair is selected — everything below is about it. + in property has-selection: false; + /// How many repairs are on this photograph. + in property count: 0; + + /// In frame units, which is what the model stores. The slider shows them + /// as a percentage of the frame's height, since "0.012" means nothing to + /// anybody and "1.2%" at least says how much of the picture is covered. + in property radius: 0.012; + in property feather: 0.35; + // `spot-opacity` and not `opacity`, which every element already has as a + // built-in: overriding it is refused by the compiler, and had it been + // allowed it would have faded the panel instead of describing the repair. + in property spot-opacity: 1.0; + /// 0 heal, 1 clone — the order the chips are listed in below. + in property mode: 0; + + callback radius-changed(float); + callback feather-changed(float); + callback opacity-changed(float); + callback mode-picked(int); + callback removed(); + + background: Theme.surface; + + // Flat rather than nested, for the reason `MaskPanel` gives: a nested + // conditional layout under-reported its height and drew rows on top of one + // another. + VerticalLayout { + padding: Theme.gap; + spacing: Theme.gap-sm; + alignment: start; + + HorizontalLayout { + PanelHeading { text: "REPAIR"; } + Rectangle { horizontal-stretch: 1; } + if root.count > 0: Value { + text: root.count + (root.count == 1 ? " spot" : " spots"); + } + } + + if !root.enabled: Caption { text: "No image"; } + + // The instruction, which is the whole interface until the first click. + // A tool whose canvas gesture is its only way in has to say so, or it + // is a mode that appears to do nothing. + if root.enabled && !root.has-selection: Caption { + text: root.count == 0 + ? "Click a mark on the photograph to cover it." + : "Click a mark to cover it, or a circle to adjust it."; + wrap: word-wrap; + } + + if root.enabled && root.has-selection: SliderRow { + label: "Size"; + hint: "% of frame"; + value: root.radius * 100; + default-value: 1.2; + minimum: 0.1; + maximum: 25; + precision: 1; + changed(v) => { root.radius-changed(v / 100); } + reset => { root.radius-changed(0.012); } + } + + if root.enabled && root.has-selection: SliderRow { + label: "Feather"; + hint: "% of size"; + value: root.feather * 100; + default-value: 35; + minimum: 0; + maximum: 100; + precision: 0; + changed(v) => { root.feather-changed(v / 100); } + reset => { root.feather-changed(0.35); } + } + + if root.enabled && root.has-selection: SliderRow { + label: "Opacity"; + value: root.spot-opacity * 100; + default-value: 100; + minimum: 0; + maximum: 100; + precision: 0; + changed(v) => { root.opacity-changed(v / 100); } + reset => { root.opacity-changed(1.0); } + } + + // Heal first, because it is the default and the right answer for dust. + // Clone is the escape for a repair that straddles an edge, where + // interpolating the boundary smears the edge across the disc. + if root.enabled && root.has-selection: Segmented { + label: "Blend"; + options: ["Heal", "Clone"]; + selected: root.mode; + picked(i) => { root.mode-picked(i); } + } + + if root.enabled && root.has-selection: Rectangle { + height: Theme.gap-sm; + } + + if root.enabled && root.has-selection: Button { + text: "Delete Repair"; + clicked => { root.removed(); } + } + } +}