From b89f1cfece888aae6a8e44ab5c021220e1ffdf48 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Wed, 26 Aug 2026 21:30:14 +0200 Subject: [PATCH] Snapshot the whole edit in the history, so a drawn mask can be taken back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The undo stack snapshotted a `Preset` — the parameter map — and a mask layer is deliberately not a parameter. So drawing one changed nothing the history could see: `record` returned `false`, no step opened, and the layer the photographer had just painted had no way back. The interface went on calling `record` in good faith, including from the mask controls, and nothing failed. A film stock went missing the same way. The snapshot is an `EditState` now, so the history is complete by construction rather than by anyone keeping a list in their head. `undo` and `redo` return a `Step` rather than a `bool`. Stepping is not the only outcome a caller has to act on — a step across a change of film leaves the graph without its tables, and only the caller can bake them — and a `bool` would let that be dropped by writing nothing at all, which is the shape of mistake this module had already made once. `DevelopSession` settles the debt either way; a step that found nowhere to go is left alone, since clearing the film because undo hit the floor would take the stock off the picture. Five tests, all of which fail against the old snapshot: a drawn layer is undoable and redoable, a layer's own settings are a step of their own, a change of stock is a step and names what it needs baked back, clearing the film is undoable, and an exposure move does not deep-copy the mask stack. Co-Authored-By: Claude Opus 5 (1M context) --- core/dr-pipeline/src/history.rs | 338 ++++++++++++++++++++++++-------- core/dr-pipeline/src/lib.rs | 2 +- core/dr-pipeline/tests/spots.rs | 6 +- docs/traceability.md | 14 +- ui/dr-ui/src/develop.rs | 29 ++- 5 files changed, 298 insertions(+), 91 deletions(-) diff --git a/core/dr-pipeline/src/history.rs b/core/dr-pipeline/src/history.rs index 5ca977f..401c883 100644 --- a/core/dr-pipeline/src/history.rs +++ b/core/dr-pipeline/src/history.rs @@ -3,10 +3,9 @@ //! //! # A stack of snapshots, not a stack of commands //! -//! The edit graph is plain data, and [`Preset::capture`] already reduces it to -//! the values that differ from default. So a history is a list of those, and -//! undo is [`Preset::apply`] at full scope — the same two calls the clipboard -//! and the sidecar are built from. +//! The edit graph is plain data, and [`EditGraph::state`] reduces it to +//! exactly what an edit is. So a history is a list of those, and undo is +//! [`EditGraph::set_state`] — the same two calls the sidecar is built from. //! //! The alternative, a command per action with an inverse beside it, would be a //! second thing every operation had to register. Operations are *declared* @@ -15,6 +14,34 @@ //! A snapshot knows nothing about which operations exist, so it cannot fall //! behind them. //! +//! # The snapshot has to be of the whole edit +//! +//! That last claim held for the operations and stopped holding for everything +//! else. This stack used to snapshot a [`Preset`](crate::Preset), which is the +//! parameter map — and a mask layer is not a parameter, a film stock is not a +//! parameter, and a repair is not a parameter, all three deliberately so. +//! +//! It went wrong three times, in two different ways, which is the argument for +//! not leaving it to anyone's memory: +//! +//! - **The masks and the stock went missing quietly.** Drawing a mask changed +//! nothing a `Preset` could see, so [`History::record`] returned `false`, no +//! step was opened, and the interface went on calling it in good faith. The +//! layer a photographer had just painted had no way back, and nothing +//! panicked, no test failed, and the only signal available was a `bool` +//! nobody was reading. +//! +//! - **The repairs would have gone missing loudly.** Undo is the single most +//! expected thing to do with a spot — place it, dislike it, take it back — +//! and a snapshot that could not carry the spot set would have stepped some +//! unrelated slider instead and left the repair on the photograph. That is +//! worse than no undo at all, because it looks like undo is *broken* rather +//! than absent. It did not happen: [`EditState`] refused to compile when the +//! spot set arrived in the graph, which is what that type is for. +//! +//! So the snapshot is an [`EditState`], and what makes that stay true is a +//! compiler error rather than a habit — see [`crate::state`]. +//! //! # What makes forty events one step //! //! A drag emits a change per frame. Recorded naively that is forty undo steps, @@ -42,15 +69,17 @@ //! # What this is not, yet //! //! FR-DEV-5 also asks for history persisted with the catalog and named -//! snapshots. This is per-session and in memory: closing the image forgets it. -//! The gap that mattered was that an automatically saved mis-drag had no way -//! back at all, and that is what this closes. +//! snapshots, and FR-DEV-7 for comparing against a chosen history state. +//! This is per-session and in memory, with no way to name a step or to jump +//! to one: closing the image forgets it. The gap that mattered was that an +//! automatically saved mis-drag had no way back at all, and that is what this +//! closes. use std::time::{Duration, Instant}; use crate::descriptor::{OpId, ParamId}; use crate::graph::EditGraph; -use crate::preset::{Preset, Scope}; +use crate::state::{EditState, FilmRebake}; /// TRACES: FR-DEV-5 | NFR-RES-1 /// How many states are held, the current one included. @@ -121,6 +150,42 @@ impl Edit { } } +/// TRACES: FR-DEV-5 +/// What one press of undo or redo did. +/// +/// Not a `bool`, because stepping is not the only outcome a caller has to act +/// on: a step that crosses a change of film leaves the graph without its +/// tables, and only the caller can bake them back (see [`FilmRebake`]). A +/// `bool` would let that be dropped by writing nothing at all, which is the +/// shape of mistake this module has already made once. +#[derive(Debug, Clone, PartialEq)] +#[must_use = "a step that is not acted on leaves the panel showing the old values"] +pub enum Step { + /// Nowhere to go — the stack is at its floor, or at its head. + Nowhere, + /// The graph now holds the neighbouring state, and this is what it is + /// still owed. + Took(FilmRebake), +} + +impl Step { + /// Whether the graph moved. + /// + /// The panel has to be rebuilt when it did: undo replaces the values the + /// controls are showing and nothing here pushes them. + pub fn moved(&self) -> bool { + matches!(self, Self::Took(_)) + } + + /// The film this step needs baked back, if any. + pub fn rebake(&self) -> Option<&crate::state::FilmRef> { + match self { + Self::Nowhere => None, + Self::Took(film) => film.wanted(), + } + } +} + /// TRACES: FR-DEV-5 /// One image's undo stack. /// @@ -129,7 +194,7 @@ impl Edit { /// where redo goes. `states` is never empty — the state the history was opened /// on is the floor, and undo stops there rather than at nothing. pub struct History { - states: Vec, + states: Vec, cursor: usize, /// What produced `states[cursor]`, and when — the pair that decides /// whether the next change amends it. `None` means the current step is @@ -137,51 +202,11 @@ pub struct History { last: Option<(Edit, Instant)>, } -/// TRACES: FR-DEV-5 | FR-DEV-8 -/// One remembered state of an edit. -/// -/// # Why this is not simply a `Preset` -/// -/// It was, and that was right while every edit in the graph was a parameter. -/// A [`Preset`] is a map of scalars, which is exactly what the module -/// documentation above says a snapshot should be — nothing to register, nothing -/// to fall behind the operation set. -/// -/// A repair is not a scalar (see [`crate::spot`]), and undo is the single most -/// expected thing to do with one: place a spot, dislike it, take it back. If -/// the snapshot could not carry the spot set, that press would step some -/// unrelated slider instead and leave the repair on the photograph — which is -/// worse than no undo at all, because it looks like undo is broken rather than -/// absent. -/// -/// So the state is the pair. The same door is what the mask stack will come -/// through — masks are outside undo today for precisely this reason, and -/// FR-DEV-5 is not finished until they are not. -#[derive(Debug, Clone, PartialEq)] -struct State { - params: Preset, - spots: crate::spot::SpotSet, -} - -impl State { - fn capture(graph: &EditGraph) -> Self { - Self { - params: Preset::capture(graph), - spots: graph.spots().clone(), - } - } - - fn apply(&self, graph: &mut EditGraph) { - self.params.apply(graph, Scope::Everything); - *graph.spots_mut() = self.spots.clone(); - } -} - impl History { /// Start from the state `graph` is in. pub fn new(graph: &EditGraph) -> Self { Self { - states: vec![State::capture(graph)], + states: vec![graph.state()], cursor: 0, last: None, } @@ -208,7 +233,7 @@ impl History { /// [`Self::record`] with the clock supplied, so coalescing can be tested /// without sleeping. pub fn record_at(&mut self, graph: &EditGraph, edit: Edit, at: Instant) -> bool { - let state = State::capture(graph); + let state = graph.state(); if self.states.get(self.cursor) == Some(&state) { return false; } @@ -262,36 +287,37 @@ impl History { self.cursor + 1 < self.states.len() } - /// Step `graph` back one, returning whether there was anywhere to go. + /// Step `graph` back one. /// - /// Applied at [`Scope::Everything`]: framing is as undoable as colour, and - /// a crop drag the user wants back is one of the likelier reasons to - /// reach for this. The view survives, because [`Preset::apply`] preserves - /// it — an undo that also jumped the viewport would read as navigation. - pub fn undo(&mut self, graph: &mut EditGraph) -> bool { + /// The whole edit goes back, not merely its colour: framing is as undoable + /// as exposure, a mask as undoable as either, and a crop drag the user + /// wants back is one of the likelier reasons to reach for this. The view + /// survives, because [`EditGraph::set_state`] preserves it — an undo that + /// also jumped the viewport would read as navigation. + pub fn undo(&mut self, graph: &mut EditGraph) -> Step { let Some(target) = self.cursor.checked_sub(1) else { - return false; + return Step::Nowhere; }; self.restore(graph, target) } - /// Step `graph` forward one, returning whether there was anywhere to go. - pub fn redo(&mut self, graph: &mut EditGraph) -> bool { + /// Step `graph` forward one. + pub fn redo(&mut self, graph: &mut EditGraph) -> Step { self.restore(graph, self.cursor + 1) } - fn restore(&mut self, graph: &mut EditGraph, target: usize) -> bool { + fn restore(&mut self, graph: &mut EditGraph, target: usize) -> Step { let Some(state) = self.states.get(target).cloned() else { - return false; + return Step::Nowhere; }; - state.apply(graph); + let rebake = graph.set_state(&state); self.cursor = target; // Closes the current step. Without this, a slider moved immediately // after an undo would fold into the step it was just undone out of — // and the state the user had just recovered would be overwritten by // the very edit they made from it. self.last = None; - true + Step::Took(rebake) } /// How many states are held, the current one included. @@ -307,9 +333,48 @@ impl History { mod tests { use super::*; use crate::framing; + use crate::graph::Film; + use crate::mask::{MaskLayer, MaskSource}; use crate::ops::{curve, exposure, saturation}; + use crate::state::FilmRef; use crate::CropRect; + /// A radial layer, the cheapest thing a photographer can put on a picture + /// that is not a number. + fn layer(id: &str) -> MaskLayer { + MaskLayer::new( + id, + MaskSource::Radial { + centre: (0.5, 0.5), + radii: (0.25, 0.25), + angle: 0.0, + feather: 0.2, + }, + ) + } + + /// A stock with tables well-formed enough for the film node to keep them. + /// The numbers are not a real emulsion and do not need to be — what is + /// under test is whether the *choice* survives a step. + fn film(stock: &str) -> Film { + Film { + stock: stock.to_string(), + print: None, + tables: crate::ops::FilmTables { + exposure_matrix: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + curves: vec![[0.5, 0.5, 0.5]; crate::ops::film_sim::CURVE_SAMPLES], + curve_log_min: -3.0, + curve_log_max: 1.0, + lut: vec![[0.5, 0.5, 0.5]; 8], + density_max: 2.0, + lut_size: 2, + grain_particles: [0.0; 3], + grain_density_max: [2.0; 3], + grain_uniformity: 1.0, + }, + } + } + fn at(base: Instant, ms: u64) -> Instant { base + Duration::from_millis(ms) } @@ -328,6 +393,125 @@ mod tests { } } + #[test] + fn a_mask_the_user_just_drew_can_be_taken_back() { + // The bug this module was rewritten for. A snapshot used to be a + // `Preset` — the parameter map — and a mask layer is deliberately not + // a parameter, so drawing one changed nothing the history could see: + // `record` returned `false`, no step opened, and the layer had no way + // back. Nothing failed; the undo simply was not there. + let mut g = EditGraph::default_chain(); + let mut h = History::new(&g); + + g.masks_mut().push(layer("l1")); + assert!( + h.record(&g, Edit::Discrete), + "drawing a mask has to open a step" + ); + + assert!(h.undo(&mut g).moved()); + assert!( + g.masks().is_empty(), + "the layer survived an undo: {} left", + g.masks().len() + ); + assert!(h.redo(&mut g).moved()); + assert_eq!(g.masks().len(), 1, "and redo has to put it back"); + } + + #[test] + fn editing_a_layer_is_a_step_of_its_own() { + // Not merely adding and removing: the settings *inside* a layer are + // the part a photographer works at, and they are as far from being a + // graph parameter as the layer itself. + let mut g = EditGraph::default_chain(); + g.masks_mut().push(layer("l1")); + let mut h = History::new(&g); + + if let Some(l) = g.masks_mut().get_mut("l1") { + l.opacity = 0.4; + } + assert!(h.record(&g, Edit::Op(OpId("mask-opacity")))); + + assert!(h.undo(&mut g).moved()); + let back = g.masks().get("l1").expect("the layer itself must remain"); + assert!( + (back.opacity - 1.0).abs() < 1e-6, + "the opacity did not come back: {}", + back.opacity + ); + } + + #[test] + fn undoing_across_a_change_of_film_asks_for_the_stock_back() { + // A stock is not a scalar either, so it went missing the same way. It + // needs the extra half-step because this crate cannot bake tables — + // the step reports what it owes rather than leaving the caller to + // remember, which `Version::update` is the standing evidence for. + let mut g = EditGraph::default_chain(); + g.set_film(Some(film("kodak_portra_400"))); + let mut h = History::new(&g); + + g.set_film(Some(film("ilford_hp5"))); + assert!(h.record(&g, Edit::Discrete), "a change of stock is a step"); + + let step = h.undo(&mut g); + assert!(step.moved()); + assert_eq!( + step.rebake(), + Some(&FilmRef { + stock: "kodak_portra_400".into(), + print: None, + }), + "undo must name the stock it needs baked back" + ); + } + + #[test] + fn clearing_the_film_is_undoable_and_the_floor_has_none_to_restore() { + let mut g = EditGraph::default_chain(); + let mut h = History::new(&g); + + g.set_film(Some(film("kodak_portra_400"))); + assert!(h.record(&g, Edit::Discrete)); + + let step = h.undo(&mut g); + assert!(step.moved()); + assert_eq!( + step.rebake(), + None, + "there was no film to go back to, so nothing is owed" + ); + assert!(g.film().is_none()); + } + + #[test] + fn a_step_that_leaves_the_masks_alone_does_not_copy_them() { + // The reason `EditState` shares its stack rather than owning it. + // `record` runs on every parameter change, which during a drag is once + // a frame; a painted brush is thousands of stroke points, and + // deep-copying it sixty times a second to note an exposure move would + // be a cost paid for nothing at all. + let mut g = EditGraph::default_chain(); + g.masks_mut().push(layer("l1")); + + let before = g.state(); + g.set_param(exposure::ID, exposure::EXPOSURE, 1.0); + let after = g.state(); + + assert!( + std::sync::Arc::ptr_eq(&before.masks, &after.masks), + "an exposure move deep-copied the mask stack" + ); + + // And the sharing ends the moment a layer is actually touched, or the + // two snapshots would be the same object and undo would restore + // nothing. + g.masks_mut().push(layer("l2")); + assert!(!std::sync::Arc::ptr_eq(&before.masks, &g.state().masks)); + assert_eq!(before.masks.len(), 1, "the older snapshot was mutated"); + } + #[test] fn a_whole_drag_of_one_slider_is_a_single_undo_step() { // The reason this type is not a plain stack. A drag emits an event per @@ -340,7 +524,7 @@ mod tests { drag(&mut h, &mut g, base, 0.0, 1.5, 40); assert_eq!(h.depth(), 2, "the drag should have added exactly one state"); - assert!(h.undo(&mut g)); + assert!(h.undo(&mut g).moved()); assert_eq!(g.param(exposure::ID, exposure::EXPOSURE), Some(0.0)); assert!(!h.can_undo(), "undo went behind the state it opened in"); } @@ -366,7 +550,7 @@ mod tests { ); assert_eq!(h.depth(), 3); - assert!(h.undo(&mut g)); + assert!(h.undo(&mut g).moved()); assert_eq!(g.param(exposure::ID, exposure::EXPOSURE), Some(1.0)); } @@ -389,7 +573,7 @@ mod tests { at(base, 16), ); - assert!(h.undo(&mut g)); + assert!(h.undo(&mut g).moved()); assert_eq!(g.param(saturation::ID, saturation::SATURATION), Some(0.0)); assert_eq!( g.param(exposure::ID, exposure::EXPOSURE), @@ -446,7 +630,7 @@ mod tests { } assert_eq!(h.depth(), 2); - assert!(h.undo(&mut g)); + assert!(h.undo(&mut g).moved()); assert!( g.crop().is_full(), "the frame did not come back: {:?}", @@ -502,11 +686,11 @@ mod tests { g.set_param(saturation::ID, saturation::SATURATION, -40.0); g.set_param(framing::ID, framing::ANGLE, 3.0); h.record(&g, Edit::Discrete); - let after = Preset::capture(&g); + let after = g.state(); - assert!(h.undo(&mut g)); - assert!(h.redo(&mut g)); - assert_eq!(Preset::capture(&g), after); + assert!(h.undo(&mut g).moved()); + assert!(h.redo(&mut g).moved()); + assert_eq!(g.state(), after); assert!(!h.can_redo()); } @@ -521,7 +705,7 @@ mod tests { g.set_param(exposure::ID, exposure::EXPOSURE, 1.0); h.record(&g, Edit::Discrete); - assert!(h.undo(&mut g)); + assert!(h.undo(&mut g).moved()); g.set_param(saturation::ID, saturation::SATURATION, 25.0); h.record(&g, Edit::Discrete); @@ -549,7 +733,7 @@ mod tests { at(base, 2000), ); - assert!(h.undo(&mut g)); + assert!(h.undo(&mut g).moved()); assert_eq!(g.param(exposure::ID, exposure::EXPOSURE), Some(1.0)); g.set_param(exposure::ID, exposure::EXPOSURE, 1.1); @@ -559,7 +743,7 @@ mod tests { at(base, 2010), ); - assert!(h.undo(&mut g)); + assert!(h.undo(&mut g).moved()); assert_eq!( g.param(exposure::ID, exposure::EXPOSURE), Some(1.0), @@ -586,7 +770,7 @@ mod tests { // Every step still present is reachable, and the walk stops at the // floor rather than running off the end. let mut steps = 0; - while h.undo(&mut g) { + while h.undo(&mut g).moved() { steps += 1; } assert_eq!(steps, DEPTH - 1); @@ -627,7 +811,7 @@ mod tests { width: 0.25, height: 0.25, }); - assert!(h.undo(&mut g)); + assert!(h.undo(&mut g).moved()); assert!(g.framing().is_zoomed(), "the undo threw away the viewport"); } diff --git a/core/dr-pipeline/src/lib.rs b/core/dr-pipeline/src/lib.rs index 431fc3e..eef87d5 100644 --- a/core/dr-pipeline/src/lib.rs +++ b/core/dr-pipeline/src/lib.rs @@ -54,7 +54,7 @@ pub use detail::{ }; pub use framing::{CropRect, Framing}; pub use graph::{EditGraph, OpCapability, ParamCapability}; -pub use history::{Edit, History}; +pub use history::{Edit, History, Step}; pub use lens::{compose_warps, ComposedWarp, Warp}; pub use operation::{ compose, compose_with_framing, Affects, ComposedShader, Helper, Invalidation, Operation, diff --git a/core/dr-pipeline/tests/spots.rs b/core/dr-pipeline/tests/spots.rs index 5bc61c8..ec6b392 100644 --- a/core/dr-pipeline/tests/spots.rs +++ b/core/dr-pipeline/tests/spots.rs @@ -259,10 +259,10 @@ fn undo_takes_a_repair_back() { assert!(history.record(&graph, Edit::Discrete)); assert_eq!(graph.spots().len(), 1); - assert!(history.undo(&mut graph)); + assert!(history.undo(&mut graph).moved()); assert_eq!(graph.spots().len(), 0, "the repair is still on the frame"); - assert!(history.redo(&mut graph)); + assert!(history.redo(&mut graph).moved()); assert_eq!(graph.spots().len(), 1, "and redo could not put it back"); } @@ -287,7 +287,7 @@ fn undo_steps_back_through_a_moved_source() { .set_offset((0.2, 0.1)); history.record(&graph, Edit::Discrete); - assert!(history.undo(&mut graph)); + assert!(history.undo(&mut graph).moved()); assert_eq!( graph.spots().get(&id).map(|s| s.offset), Some((0.08, 0.0)), diff --git a/docs/traceability.md b/docs/traceability.md index 2b61bf5..36a0b10 100644 --- a/docs/traceability.md +++ b/docs/traceability.md @@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n | Metric | Value | |---|---| | Source files scanned | 228 | -| TRACES tags found | 629 | +| TRACES tags found | 631 | | Requirements defined | 177 | | Requirements covered | 91 | | **Coverage** | **51.4%** (91/177) | @@ -52,21 +52,21 @@ _None._ | 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:134`](../core/dr-pipeline/src/sidecar.rs#L134), [`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:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`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:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`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:158`](../core/dr-pipeline/src/graph.rs#L158), [`core/dr-pipeline/src/graph.rs:547`](../core/dr-pipeline/src/graph.rs#L547), [`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:473`](../core/dr-pipeline/src/operation.rs#L473), [`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:1431`](../core/dr-pipeline/src/sidecar.rs#L1431), [`core/dr-pipeline/src/sidecar.rs:1491`](../core/dr-pipeline/src/sidecar.rs#L1491), [`core/dr-pipeline/src/sidecar.rs:155`](../core/dr-pipeline/src/sidecar.rs#L155), [`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:1584`](../ui/dr-ui/src/develop.rs#L1584), [`ui/dr-ui/src/develop.rs:1599`](../ui/dr-ui/src/develop.rs#L1599), [`ui/dr-ui/src/develop.rs:1621`](../ui/dr-ui/src/develop.rs#L1621), [`ui/dr-ui/src/develop.rs:1753`](../ui/dr-ui/src/develop.rs#L1753), [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`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:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:3214`](../ui/dr-ui/src/develop.rs#L3214), [`ui/dr-ui/src/develop.rs:3268`](../ui/dr-ui/src/develop.rs#L3268), [`ui/dr-ui/src/develop.rs:3312`](../ui/dr-ui/src/develop.rs#L3312), [`ui/dr-ui/src/develop.rs:3362`](../ui/dr-ui/src/develop.rs#L3362), [`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-3 | [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`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:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`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:158`](../core/dr-pipeline/src/graph.rs#L158), [`core/dr-pipeline/src/graph.rs:547`](../core/dr-pipeline/src/graph.rs#L547), [`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:473`](../core/dr-pipeline/src/operation.rs#L473), [`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:1431`](../core/dr-pipeline/src/sidecar.rs#L1431), [`core/dr-pipeline/src/sidecar.rs:1491`](../core/dr-pipeline/src/sidecar.rs#L1491), [`core/dr-pipeline/src/sidecar.rs:155`](../core/dr-pipeline/src/sidecar.rs#L155), [`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:1584`](../ui/dr-ui/src/develop.rs#L1584), [`ui/dr-ui/src/develop.rs:1599`](../ui/dr-ui/src/develop.rs#L1599), [`ui/dr-ui/src/develop.rs:1621`](../ui/dr-ui/src/develop.rs#L1621), [`ui/dr-ui/src/develop.rs:1753`](../ui/dr-ui/src/develop.rs#L1753), [`ui/dr-ui/src/develop.rs:1831`](../ui/dr-ui/src/develop.rs#L1831), [`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:2762`](../ui/dr-ui/src/develop.rs#L2762), [`ui/dr-ui/src/develop.rs:3237`](../ui/dr-ui/src/develop.rs#L3237), [`ui/dr-ui/src/develop.rs:3291`](../ui/dr-ui/src/develop.rs#L3291), [`ui/dr-ui/src/develop.rs:3335`](../ui/dr-ui/src/develop.rs#L3335), [`ui/dr-ui/src/develop.rs:3385`](../ui/dr-ui/src/develop.rs#L3385), [`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:229`](../core/dr-pipeline/src/graph.rs#L229), [`core/dr-pipeline/src/graph.rs:22`](../core/dr-pipeline/src/graph.rs#L22), [`core/dr-pipeline/src/graph.rs:44`](../core/dr-pipeline/src/graph.rs#L44), [`core/dr-pipeline/src/graph.rs:57`](../core/dr-pipeline/src/graph.rs#L57), [`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-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:57`](../core/dr-pipeline/src/graph.rs#L57), [`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:229`](../core/dr-pipeline/src/graph.rs#L229), [`core/dr-pipeline/src/graph.rs:44`](../core/dr-pipeline/src/graph.rs#L44), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:3941`](../ui/dr-ui/src/develop.rs#L3941) | +| 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:229`](../core/dr-pipeline/src/graph.rs#L229), [`core/dr-pipeline/src/graph.rs:44`](../core/dr-pipeline/src/graph.rs#L44), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:3964`](../ui/dr-ui/src/develop.rs#L3964) | | 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:433`](../core/dr-gpu/tests/capture_sharpen.rs#L433), [`core/dr-gpu/tests/detail_stage.rs:241`](../core/dr-gpu/tests/detail_stage.rs#L241), [`core/dr-gpu/tests/local_contrast.rs:475`](../core/dr-gpu/tests/local_contrast.rs#L475), [`core/dr-gpu/tests/noise_reduction.rs:555`](../core/dr-gpu/tests/noise_reduction.rs#L555), [`core/dr-pipeline/src/framing.rs:188`](../core/dr-pipeline/src/framing.rs#L188), [`core/dr-pipeline/src/graph.rs:576`](../core/dr-pipeline/src/graph.rs#L576), [`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:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`core/dr-pipeline/src/operation.rs:1491`](../core/dr-pipeline/src/operation.rs#L1491), [`core/dr-pipeline/src/operation.rs:1516`](../core/dr-pipeline/src/operation.rs#L1516), [`core/dr-pipeline/src/operation.rs:1531`](../core/dr-pipeline/src/operation.rs#L1531), [`core/dr-pipeline/src/operation.rs:1555`](../core/dr-pipeline/src/operation.rs#L1555), [`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:549`](../core/dr-pipeline/src/operation.rs#L549) | | 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:100`](../core/dr-pipeline/src/graph.rs#L100), [`core/dr-pipeline/src/graph.rs:114`](../core/dr-pipeline/src/graph.rs#L114), [`core/dr-pipeline/src/graph.rs:303`](../core/dr-pipeline/src/graph.rs#L303), [`core/dr-pipeline/src/operation.rs:1014`](../core/dr-pipeline/src/operation.rs#L1014), [`core/dr-pipeline/src/operation.rs:1043`](../core/dr-pipeline/src/operation.rs#L1043), [`core/dr-pipeline/src/operation.rs:1410`](../core/dr-pipeline/src/operation.rs#L1410), [`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:166`](../core/dr-pipeline/src/sidecar.rs#L166), [`core/dr-pipeline/src/sidecar.rs:1752`](../core/dr-pipeline/src/sidecar.rs#L1752), [`core/dr-pipeline/src/sidecar.rs:1828`](../core/dr-pipeline/src/sidecar.rs#L1828), [`core/dr-pipeline/src/sidecar.rs:434`](../core/dr-pipeline/src/sidecar.rs#L434), [`core/dr-pipeline/src/sidecar.rs:560`](../core/dr-pipeline/src/sidecar.rs#L560), [`core/dr-pipeline/src/sidecar.rs:683`](../core/dr-pipeline/src/sidecar.rs#L683), [`core/dr-pipeline/src/state.rs:100`](../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:55`](../core/dr-pipeline/src/state.rs#L55), [`core/dr-pipeline/src/state.rs:95`](../core/dr-pipeline/src/state.rs#L95), [`ui/dr-ui/src/develop.rs:2340`](../ui/dr-ui/src/develop.rs#L2340), [`ui/dr-ui/src/develop.rs:2357`](../ui/dr-ui/src/develop.rs#L2357), [`ui/dr-ui/src/develop.rs:2388`](../ui/dr-ui/src/develop.rs#L2388), [`ui/dr-ui/src/develop.rs:2485`](../ui/dr-ui/src/develop.rs#L2485), [`ui/dr-ui/src/develop.rs:2798`](../ui/dr-ui/src/develop.rs#L2798), [`ui/dr-ui/src/develop.rs:2813`](../ui/dr-ui/src/develop.rs#L2813), [`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-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/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`core/dr-pipeline/src/graph.rs:361`](../core/dr-pipeline/src/graph.rs#L361), [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`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), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:70`](../core/dr-pipeline/src/state.rs#L70), [`ui/dr-ui/src/develop.rs:2837`](../ui/dr-ui/src/develop.rs#L2837), [`ui/dr-ui/src/develop.rs:2847`](../ui/dr-ui/src/develop.rs#L2847), [`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-5 | [`core/dr-pipeline/src/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`core/dr-pipeline/src/graph.rs:361`](../core/dr-pipeline/src/graph.rs#L361), [`core/dr-pipeline/src/history.rs:141`](../core/dr-pipeline/src/history.rs#L141), [`core/dr-pipeline/src/history.rs:177`](../core/dr-pipeline/src/history.rs#L177), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:72`](../core/dr-pipeline/src/history.rs#L72), [`core/dr-pipeline/src/history.rs:88`](../core/dr-pipeline/src/history.rs#L88), [`core/dr-pipeline/src/history.rs:96`](../core/dr-pipeline/src/history.rs#L96), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:70`](../core/dr-pipeline/src/state.rs#L70), [`ui/dr-ui/src/develop.rs:2813`](../ui/dr-ui/src/develop.rs#L2813), [`ui/dr-ui/src/develop.rs:2843`](../ui/dr-ui/src/develop.rs#L2843), [`ui/dr-ui/src/develop.rs:2856`](../ui/dr-ui/src/develop.rs#L2856), [`ui/dr-ui/src/develop.rs:2868`](../ui/dr-ui/src/develop.rs#L2868), [`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:2756`](../ui/dr-ui/src/develop.rs#L2756), [`ui/dr-ui/src/develop.rs:2777`](../ui/dr-ui/src/develop.rs#L2777), [`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-pipeline/src/detail.rs:364`](../core/dr-pipeline/src/detail.rs#L364), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299) | -| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2090`](../core/dr-gpu/src/adjust.rs#L2090), [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:2252`](../core/dr-gpu/src/adjust.rs#L2252), [`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:199`](../core/dr-gpu/tests/capture_sharpen.rs#L199), [`core/dr-gpu/tests/detail_stage.rs:327`](../core/dr-gpu/tests/detail_stage.rs#L327), [`core/dr-gpu/tests/local_contrast.rs:262`](../core/dr-gpu/tests/local_contrast.rs#L262), [`core/dr-gpu/tests/noise_reduction.rs:377`](../core/dr-gpu/tests/noise_reduction.rs#L377), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/graph.rs:517`](../core/dr-pipeline/src/graph.rs#L517), [`core/dr-pipeline/src/graph.rs:547`](../core/dr-pipeline/src/graph.rs#L547), [`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:647`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L647), [`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:658`](../core/dr-pipeline/src/ops/local_contrast.rs#L658), [`core/dr-pipeline/src/ops/noise_reduction.rs:691`](../core/dr-pipeline/src/ops/noise_reduction.rs#L691), [`ui/dr-ui/src/develop.rs:2161`](../ui/dr-ui/src/develop.rs#L2161), [`ui/dr-ui/src/develop.rs:3003`](../ui/dr-ui/src/develop.rs#L3003), [`ui/dr-ui/src/develop.rs:3486`](../ui/dr-ui/src/develop.rs#L3486), [`ui/dr-ui/src/develop.rs:3520`](../ui/dr-ui/src/develop.rs#L3520), [`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-DSP-1 | [`core/dr-gpu/src/adjust.rs:2090`](../core/dr-gpu/src/adjust.rs#L2090), [`core/dr-gpu/src/adjust.rs:2167`](../core/dr-gpu/src/adjust.rs#L2167), [`core/dr-gpu/src/adjust.rs:2252`](../core/dr-gpu/src/adjust.rs#L2252), [`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:199`](../core/dr-gpu/tests/capture_sharpen.rs#L199), [`core/dr-gpu/tests/detail_stage.rs:327`](../core/dr-gpu/tests/detail_stage.rs#L327), [`core/dr-gpu/tests/local_contrast.rs:262`](../core/dr-gpu/tests/local_contrast.rs#L262), [`core/dr-gpu/tests/noise_reduction.rs:377`](../core/dr-gpu/tests/noise_reduction.rs#L377), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:439`](../core/dr-pipeline/src/detail.rs#L439), [`core/dr-pipeline/src/graph.rs:517`](../core/dr-pipeline/src/graph.rs#L517), [`core/dr-pipeline/src/graph.rs:547`](../core/dr-pipeline/src/graph.rs#L547), [`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:647`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L647), [`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:658`](../core/dr-pipeline/src/ops/local_contrast.rs#L658), [`core/dr-pipeline/src/ops/noise_reduction.rs:691`](../core/dr-pipeline/src/ops/noise_reduction.rs#L691), [`ui/dr-ui/src/develop.rs:2161`](../ui/dr-ui/src/develop.rs#L2161), [`ui/dr-ui/src/develop.rs:3026`](../ui/dr-ui/src/develop.rs#L3026), [`ui/dr-ui/src/develop.rs:3509`](../ui/dr-ui/src/develop.rs#L3509), [`ui/dr-ui/src/develop.rs:3543`](../ui/dr-ui/src/develop.rs#L3543), [`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-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:2206`](../ui/dr-ui/src/develop.rs#L2206), [`ui/dr-ui/src/develop.rs:4588`](../ui/dr-ui/src/develop.rs#L4588), [`ui/dr-ui/src/develop.rs:4620`](../ui/dr-ui/src/develop.rs#L4620), [`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:2206`](../ui/dr-ui/src/develop.rs#L2206), [`ui/dr-ui/src/develop.rs:4611`](../ui/dr-ui/src/develop.rs#L4611), [`ui/dr-ui/src/develop.rs:4643`](../ui/dr-ui/src/develop.rs#L4643), [`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-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:2400`](../core/dr-gpu/src/adjust.rs#L2400), [`core/dr-pipeline/src/graph.rs:507`](../core/dr-pipeline/src/graph.rs#L507), [`core/dr-pipeline/src/graph.rs:560`](../core/dr-pipeline/src/graph.rs#L560), [`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) | @@ -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:72`](../core/dr-pipeline/src/history.rs#L72), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59) | | 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 725a760..a4ba8ba 100644 --- a/ui/dr-ui/src/develop.rs +++ b/ui/dr-ui/src/develop.rs @@ -3163,7 +3163,7 @@ impl DevelopSession { self.history.reset(&self.graph); } - /// TRACES: FR-DEV-3f + /// TRACES: FR-DEV-3f | FR-DEV-5 /// Pay what a restored edit owes the picture. /// /// `dr-pipeline` restores a stock's *name* and clears its tables, because @@ -3176,6 +3176,12 @@ impl DevelopSession { /// on holding textures that nothing will sample. That is the two halves /// disagreeing, which is the failure `set_film` exists to make /// impossible — and it is silent in this direction, which is worse. + /// + /// Baked unconditionally rather than only when the stock changed: the + /// tables come from the film node's own exposure sliders as well as from + /// the stock, and restoring an edit replaces those sliders too. A bake is + /// milliseconds and this happens on a keypress, so the cheap correct rule + /// beats the clever one. fn pay_film_debt(&mut self, rebake: &dr_pipeline::FilmRebake) { match rebake.wanted() { Some(film) => { @@ -3187,6 +3193,19 @@ impl DevelopSession { } } + /// TRACES: FR-DEV-5 + /// [`Self::pay_film_debt`] for a history step, when the step went + /// anywhere. + /// + /// The guard is the whole difference between the two: a step that found + /// nowhere to go left the graph alone, and clearing the film because + /// undo hit the floor would take the picture's stock off it. + fn settle(&mut self, step: &dr_pipeline::Step) { + if let dr_pipeline::Step::Took(rebake) = step { + self.pay_film_debt(rebake); + } + } + /// TRACES: FR-DEV-5 /// Step the edit back one, returning whether anything moved. /// @@ -3194,13 +3213,17 @@ impl DevelopSession { /// reason a paste must: this moves values the controls are showing and /// nothing here pushes them. pub fn undo(&mut self) -> bool { - self.history.undo(&mut self.graph) + let step = self.history.undo(&mut self.graph); + self.settle(&step); + step.moved() } /// TRACES: FR-DEV-5 /// Step the edit forward one, returning whether anything moved. pub fn redo(&mut self) -> bool { - self.history.redo(&mut self.graph) + let step = self.history.redo(&mut self.graph); + self.settle(&step); + step.moved() } pub fn can_undo(&self) -> bool {