Hand out descriptors a declaration could produce
`Operation::descriptor()` returned `&'static OpDescriptor`, and that lifetime
is the whole reason a build-time node is free and a run-time node is
impossible: only a compile-time literal can satisfy it, so no amount of
reading `ops/*.yaml` at startup could ever produce a descriptor the rest of
the application would accept. FR-PLG-2 says a bundled operation and a
third-party plugin are the same kind of thing, differing only in where the
file was found — and a lifetime outsiders cannot meet is exactly the second,
weaker format that requirement forbids.
So a descriptor is now owned and handed out as `Arc<OpDescriptor>`, with `Vec`
where it held `&'static` slices. `Arc` rather than a `&self`-borrowed
reference because the callers want to *keep* it: the develop panel collects
descriptors and then mutates the graph, and a borrow would tie the
descriptor's lifetime to a borrow of the operation it came from, which is the
one thing `&'static` was doing right.
The identifier newtypes deliberately did not follow. `ParamId` is `Copy`, is
compared in `match` arms against generated constants, is a map key in the
sidecar and history, and reaches Slint model rows; an `Arc<str>` there would
cost a refcount on every one of those and would take `match id { EXPOSURE =>
.. }` away from the generated code. They gain an interner instead, which is
honest about its lifetime rather than pretending to one — the set of ids is
bounded by deduplication and is process-lifetime by construction, because the
sidecar on disk names its parameters and an id has to stay resolvable for as
long as any edit naming it can be opened.
No behaviour changes. Every descriptor that was a `static` is a `LazyLock`
initialiser now, `Operation::helpers` borrows from `self` instead of being
`'static` so a future run-time node can own its list, and `Warp` and `Framing`
follow `Operation` so there is one shape rather than two.
The one place a descriptor is read per frame is `compose_full`, which takes
`descriptor().id` to prefix each active operation's uniforms, and `dr-ui`
composes on every frame it draws. That is a dozen atomic increments beside a
composition that is already building several kilobytes of WGSL on the same
call; it is noted at the trait method rather than left for a profiler to find.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+25
-18
@@ -18,7 +18,7 @@
|
|||||||
//!
|
//!
|
||||||
//! It does not change the runtime model. The generated code implements the
|
//! It does not change the runtime model. The generated code implements the
|
||||||
//! same [`Operation`](../src/operation.rs) trait, publishes the same
|
//! same [`Operation`](../src/operation.rs) trait, publishes the same
|
||||||
//! `&'static OpDescriptor`, and composes through the same fused-shader path.
|
//! `Arc<OpDescriptor>`, and composes through the same fused-shader path.
|
||||||
//! Nothing downstream — not `dr-gpu`, not the develop panel — can tell a
|
//! Nothing downstream — not `dr-gpu`, not the develop panel — can tell a
|
||||||
//! declared node from a hand-written one, which is what allows the two to sit
|
//! declared node from a hand-written one, which is what allows the two to sit
|
||||||
//! side by side in one chain.
|
//! side by side in one chain.
|
||||||
@@ -788,7 +788,7 @@ fn param_ctor(
|
|||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
(
|
(
|
||||||
format!(
|
format!(
|
||||||
"ParamDescriptor::choice({:?}, {:?}, &[{}])",
|
"ParamDescriptor::choice({:?}, {:?}, vec![{}])",
|
||||||
id,
|
id,
|
||||||
label,
|
label,
|
||||||
keys.join(", ")
|
keys.join(", ")
|
||||||
@@ -1495,7 +1495,8 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
|
|||||||
\x20 Scale, Unit, WidgetDemand, WidgetKind,\n\
|
\x20 Scale, Unit, WidgetDemand, WidgetKind,\n\
|
||||||
\x20 };\n\
|
\x20 };\n\
|
||||||
\x20 #[allow(unused_imports)]\n\
|
\x20 #[allow(unused_imports)]\n\
|
||||||
\x20 use crate::operation::{Helper, Operation, Uniform};\n\n",
|
\x20 use crate::operation::{Helper, Operation, Uniform};\n\
|
||||||
|
\x20 use std::sync::{Arc, LazyLock};\n\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ids as consts, so a caller names a parameter through the type system
|
// Ids as consts, so a caller names a parameter through the type system
|
||||||
@@ -1511,20 +1512,26 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Descriptor.
|
// Descriptor.
|
||||||
let _ = writeln!(
|
//
|
||||||
out,
|
// Built once, behind a `LazyLock`, and handed out as an `Arc` clone. It
|
||||||
"\n static DESCRIPTOR: OpDescriptor = OpDescriptor {{\n\
|
// was a plain `static` until descriptors became owned (FR-PLG-2): an
|
||||||
\x20 id: ID,\n\
|
// `OpDescriptor` now holds `Vec`s, and an `Arc` is not const-constructible
|
||||||
\x20 label: LocalizedKey({label:?}),\n\
|
// in any case. The cost is one lock check the first time an operation is
|
||||||
\x20 params: &["
|
// asked what it is, and an atomic increment thereafter.
|
||||||
|
out.push_str(
|
||||||
|
"\n static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {\n\
|
||||||
|
\x20 Arc::new(OpDescriptor {\n",
|
||||||
);
|
);
|
||||||
|
let _ = writeln!(out, " id: ID,");
|
||||||
|
let _ = writeln!(out, " label: LocalizedKey({label:?}),");
|
||||||
|
out.push_str(" params: vec![\n");
|
||||||
for p in params {
|
for p in params {
|
||||||
if let Some(doc) = &p.doc {
|
if let Some(doc) = &p.doc {
|
||||||
out.push_str(&comment(doc, "//", 12));
|
out.push_str(&comment(doc, "//", 16));
|
||||||
}
|
}
|
||||||
let _ = writeln!(out, " {},", p.ctor);
|
let _ = writeln!(out, " {},", p.ctor);
|
||||||
}
|
}
|
||||||
out.push_str(" ],\n");
|
out.push_str(" ],\n");
|
||||||
|
|
||||||
// What the operation is about. The panel groups by these and names no
|
// What the operation is about. The panel groups by these and names no
|
||||||
// operation, which is what keeps FR-DEV-3a true as the set grows.
|
// operation, which is what keeps FR-DEV-3a true as the set grows.
|
||||||
@@ -1539,8 +1546,8 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
|
|||||||
format!("Attribute::{head}{}", c.as_str())
|
format!("Attribute::{head}{}", c.as_str())
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let _ = writeln!(out, " attributes: &[{}],", attrs.join(", "));
|
let _ = writeln!(out, " attributes: vec![{}],", attrs.join(", "));
|
||||||
out.push_str(" };\n");
|
out.push_str(" })\n });\n");
|
||||||
|
|
||||||
// Helpers: node-local definitions first, then the assembled list.
|
// Helpers: node-local definitions first, then the assembled list.
|
||||||
for h in local_helpers {
|
for h in local_helpers {
|
||||||
@@ -1583,7 +1590,7 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
|
|||||||
// The trait.
|
// The trait.
|
||||||
let _ = writeln!(out, "\n impl Operation for {ty} {{");
|
let _ = writeln!(out, "\n impl Operation for {ty} {{");
|
||||||
out.push_str(
|
out.push_str(
|
||||||
" fn descriptor(&self) -> &'static OpDescriptor {\n &DESCRIPTOR\n }\n\n",
|
" fn descriptor(&self) -> Arc<OpDescriptor> {\n DESCRIPTOR.clone()\n }\n\n",
|
||||||
);
|
);
|
||||||
|
|
||||||
out.push_str(
|
out.push_str(
|
||||||
@@ -1650,7 +1657,7 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
|
|||||||
|
|
||||||
if !helper_refs.is_empty() {
|
if !helper_refs.is_empty() {
|
||||||
out.push_str(
|
out.push_str(
|
||||||
"\n fn helpers(&self) -> &'static [Helper] {\n HELPERS\n }\n",
|
"\n fn helpers(&self) -> &[Helper] {\n HELPERS\n }\n",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1661,12 +1668,12 @@ fn emit_node(out: &mut String, node: &Node) -> Result<(), String> {
|
|||||||
out,
|
out,
|
||||||
"\n fn presentation(&self) -> Option<Presentation> {{\n\
|
"\n fn presentation(&self) -> Option<Presentation> {{\n\
|
||||||
\x20 Some(Presentation {{\n\
|
\x20 Some(Presentation {{\n\
|
||||||
\x20 widgets: &[{}],\n\
|
\x20 widgets: vec![{}],\n\
|
||||||
\x20 demand: WidgetDemand {{\n\
|
\x20 demand: WidgetDemand {{\n\
|
||||||
\x20 two_dimensional: {},\n\
|
\x20 two_dimensional: {},\n\
|
||||||
\x20 precise_pointing: {},\n\
|
\x20 precise_pointing: {},\n\
|
||||||
\x20 }},\n\
|
\x20 }},\n\
|
||||||
\x20 params: &[{}],\n\
|
\x20 params: vec![{}],\n\
|
||||||
\x20 }})\n\
|
\x20 }})\n\
|
||||||
\x20 }}",
|
\x20 }}",
|
||||||
p.widgets.join(", "),
|
p.widgets.join(", "),
|
||||||
|
|||||||
@@ -8,12 +8,75 @@
|
|||||||
//! Labels are keys, not strings: resolving them needs a localiser, and
|
//! Labels are keys, not strings: resolving them needs a localiser, and
|
||||||
//! `core/` must not depend on one (NFR-A11Y-1).
|
//! `core/` must not depend on one (NFR-A11Y-1).
|
||||||
|
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::sync::{LazyLock, Mutex};
|
||||||
|
|
||||||
|
/// TRACES: FR-PLG-2
|
||||||
|
/// Give a string read at run time the `'static` lifetime the identifier types
|
||||||
|
/// carry.
|
||||||
|
///
|
||||||
|
/// # Why the identifiers stayed `&'static str` when the descriptors did not
|
||||||
|
///
|
||||||
|
/// [`OpDescriptor`] became owned so a declaration read at *load* time can
|
||||||
|
/// produce one (FR-PLG-2). The three identifier newtypes below deliberately
|
||||||
|
/// did not follow it.
|
||||||
|
///
|
||||||
|
/// An id is not content; it is a key. [`ParamId`] is `Copy`, is compared in
|
||||||
|
/// `match` arms against the constants `build.rs` generates, is a map key in
|
||||||
|
/// the sidecar and in history, and is threaded through `dr-ui` into Slint
|
||||||
|
/// model rows. An `Arc<str>` there would put a refcount on every one of those
|
||||||
|
/// and would take `match id { EXPOSURE => .. }` away from the generated code —
|
||||||
|
/// which is precisely the inspectability of the built-in chain that keeping
|
||||||
|
/// the generated path was for.
|
||||||
|
///
|
||||||
|
/// So ids are interned instead, and interning is honest about its lifetime
|
||||||
|
/// rather than pretending to one. The set of interned ids is:
|
||||||
|
///
|
||||||
|
/// - **Bounded.** One entry per *distinct* string, deduplicated on the way in.
|
||||||
|
/// Parsing the same declaration a thousand times adds nothing after the
|
||||||
|
/// first.
|
||||||
|
/// - **Process-lifetime by construction.** A loaded declaration's vocabulary
|
||||||
|
/// is never withdrawn. Nothing unloads a plugin, and nothing could: the
|
||||||
|
/// sidecar on disk stores parameters by `(op_id, param_id)`, so an id has to
|
||||||
|
/// stay resolvable for as long as any edit naming it can be opened.
|
||||||
|
///
|
||||||
|
/// A leak whose bound is "the distinct ids this process has ever seen" is a
|
||||||
|
/// different thing from one that grows with use, and this is the first.
|
||||||
|
pub fn intern(s: &str) -> &'static str {
|
||||||
|
static POOL: LazyLock<Mutex<HashSet<&'static str>>> =
|
||||||
|
LazyLock::new(|| Mutex::new(HashSet::new()));
|
||||||
|
|
||||||
|
// A poisoned pool is still a correct pool: every entry in it is a
|
||||||
|
// `&'static str` that was interned successfully, and a panic elsewhere
|
||||||
|
// while the lock was held cannot have made one invalid. Refusing to
|
||||||
|
// intern here would turn an unrelated panic into an application that can
|
||||||
|
// no longer read a declaration.
|
||||||
|
let mut pool = POOL.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
|
if let Some(found) = pool.get(s) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
let leaked: &'static str = Box::leak(s.to_owned().into_boxed_str());
|
||||||
|
pool.insert(leaked);
|
||||||
|
leaked
|
||||||
|
}
|
||||||
|
|
||||||
/// Identifies a parameter within an operation.
|
/// Identifies a parameter within an operation.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct ParamId(pub &'static str);
|
pub struct ParamId(pub &'static str);
|
||||||
|
|
||||||
|
impl ParamId {
|
||||||
|
/// The same id, from a name read out of a declaration at load time.
|
||||||
|
///
|
||||||
|
/// Equal to `ParamId("exposure")` when the name is `"exposure"`: the
|
||||||
|
/// derived `PartialEq` compares the `str` contents, not the pointer, which
|
||||||
|
/// is what lets an interned id match a generated `match` arm. See
|
||||||
|
/// [`intern`].
|
||||||
|
pub fn interned(name: &str) -> Self {
|
||||||
|
Self(intern(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for ParamId {
|
impl fmt::Display for ParamId {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.0)
|
f.write_str(self.0)
|
||||||
@@ -24,6 +87,13 @@ impl fmt::Display for ParamId {
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct OpId(pub &'static str);
|
pub struct OpId(pub &'static str);
|
||||||
|
|
||||||
|
impl OpId {
|
||||||
|
/// The same id, from a declaration read at load time. See [`intern`].
|
||||||
|
pub fn interned(name: &str) -> Self {
|
||||||
|
Self(intern(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for OpId {
|
impl fmt::Display for OpId {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
f.write_str(self.0)
|
f.write_str(self.0)
|
||||||
@@ -34,6 +104,13 @@ impl fmt::Display for OpId {
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct LocalizedKey(pub &'static str);
|
pub struct LocalizedKey(pub &'static str);
|
||||||
|
|
||||||
|
impl LocalizedKey {
|
||||||
|
/// The same key, from a declaration read at load time. See [`intern`].
|
||||||
|
pub fn interned(key: &str) -> Self {
|
||||||
|
Self(intern(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What a slider's travel means.
|
/// What a slider's travel means.
|
||||||
///
|
///
|
||||||
/// Photographic controls are rarely linear in their underlying quantity:
|
/// Photographic controls are rarely linear in their underlying quantity:
|
||||||
@@ -170,7 +247,11 @@ pub enum ParamKind {
|
|||||||
Enum {
|
Enum {
|
||||||
/// In index order. The label is a localisation key, resolved by the
|
/// In index order. The label is a localisation key, resolved by the
|
||||||
/// frontend — `core/` must not depend on a localiser (NFR-A11Y-1).
|
/// frontend — `core/` must not depend on a localiser (NFR-A11Y-1).
|
||||||
variants: &'static [LocalizedKey],
|
///
|
||||||
|
/// Owned rather than `&'static`, for the reason [`OpDescriptor`]
|
||||||
|
/// gives: a declaration parsed at load time has nowhere to put a
|
||||||
|
/// `'static` slice.
|
||||||
|
variants: Vec<LocalizedKey>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +285,7 @@ pub struct Presentation {
|
|||||||
/// pair of sliders, and an operation that can say so gets a good control
|
/// pair of sliders, and an operation that can say so gets a good control
|
||||||
/// on a workstation and a usable one on a phone without the core knowing
|
/// on a workstation and a usable one on a phone without the core knowing
|
||||||
/// which it is talking to.
|
/// which it is talking to.
|
||||||
pub widgets: &'static [WidgetKind],
|
pub widgets: Vec<WidgetKind>,
|
||||||
/// What the preferred widget needs in order to be worth drawing.
|
/// What the preferred widget needs in order to be worth drawing.
|
||||||
///
|
///
|
||||||
/// Applies to the list as a whole rather than per entry: a frontend that
|
/// Applies to the list as a whole rather than per entry: a frontend that
|
||||||
@@ -215,7 +296,7 @@ pub struct Presentation {
|
|||||||
///
|
///
|
||||||
/// Parameters absent from this list are presented normally, so an
|
/// Parameters absent from this list are presented normally, so an
|
||||||
/// operation can pair a curve with an ordinary strength slider.
|
/// operation can pair a curve with an ordinary strength slider.
|
||||||
pub params: &'static [ParamId],
|
pub params: Vec<ParamId>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Presentation {
|
impl Presentation {
|
||||||
@@ -328,11 +409,13 @@ impl ParamDescriptor {
|
|||||||
/// holds the way it does for every other kind: index 0 is the neutral
|
/// holds the way it does for every other kind: index 0 is the neutral
|
||||||
/// choice, and an operation whose default is not its first variant has
|
/// choice, and an operation whose default is not its first variant has
|
||||||
/// listed them in the wrong order.
|
/// listed them in the wrong order.
|
||||||
pub const fn choice(
|
//
|
||||||
id: &'static str,
|
// Not `const`, unlike its four siblings, and the reason is the `Vec` in
|
||||||
label: &'static str,
|
// [`ParamKind::Enum`]: a heap allocation cannot happen in a const context.
|
||||||
variants: &'static [LocalizedKey],
|
// Nothing is lost — every descriptor now lives inside a `LazyLock`
|
||||||
) -> Self {
|
// initialiser rather than a `static`, because `descriptor()` hands out an
|
||||||
|
// `Arc` and an `Arc` is not const-constructible either.
|
||||||
|
pub fn choice(id: &'static str, label: &'static str, variants: Vec<LocalizedKey>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: ParamId(id),
|
id: ParamId(id),
|
||||||
label: LocalizedKey(label),
|
label: LocalizedKey(label),
|
||||||
@@ -366,13 +449,15 @@ impl ParamDescriptor {
|
|||||||
|
|
||||||
/// A general scalar with an explicit range and default.
|
/// A general scalar with an explicit range and default.
|
||||||
//
|
//
|
||||||
// Eight arguments, and a builder would be the usual answer — but this has
|
// Eight arguments, and a builder would be the usual answer — but eight
|
||||||
// to be `const` so descriptors can be `static`, which rules out the
|
// `&mut self` steps would be eight methods to say what one call already
|
||||||
// `&mut self` builder the pattern usually takes. (A `self`-by-value step
|
// says, and each one would be a place for a caller to forget a field. The
|
||||||
// *is* const-callable — `faceted` below is one — but eight of them would
|
// two common shapes have their own constructors above; this is the escape
|
||||||
// be eight methods to say what one call already says.) The two common
|
// hatch for the rest.
|
||||||
// shapes have their own constructors above; this is the escape hatch for
|
//
|
||||||
// the rest.
|
// Still `const` although no descriptor is a `static` any more: it costs
|
||||||
|
// nothing, and it keeps the five constructors uniform where only `choice`
|
||||||
|
// genuinely cannot be.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub const fn scalar(
|
pub const fn scalar(
|
||||||
id: &'static str,
|
id: &'static str,
|
||||||
@@ -404,10 +489,13 @@ impl ParamDescriptor {
|
|||||||
/// A method rather than a sixth constructor, because a facet is orthogonal
|
/// A method rather than a sixth constructor, because a facet is orthogonal
|
||||||
/// to the shape of the value: a faceted parameter is still an amount, or
|
/// to the shape of the value: a faceted parameter is still an amount, or
|
||||||
/// still a scalar in stops, and pairing every constructor with a faceted
|
/// still a scalar in stops, and pairing every constructor with a faceted
|
||||||
/// twin would double the list above to say one thing. Taking `self` by
|
/// twin would double the list above to say one thing.
|
||||||
/// value is what keeps it usable in the `static` descriptors — a `&mut
|
//
|
||||||
/// self` builder is what cannot be `const`.
|
// No longer `const`: a `ParamDescriptor` can now carry a `Vec` (an enum's
|
||||||
pub const fn faceted(mut self, facet: Facet) -> Self {
|
// variants), which gives the type drop glue, and assigning over a field of
|
||||||
|
// such a type is not something a const function may do. Nothing is lost —
|
||||||
|
// every descriptor is built inside a `LazyLock` initialiser now.
|
||||||
|
pub fn faceted(mut self, facet: Facet) -> Self {
|
||||||
self.facet = Some(facet);
|
self.facet = Some(facet);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -418,10 +506,10 @@ impl ParamDescriptor {
|
|||||||
/// bounds, or a sidecar written by a newer version with a wider range,
|
/// bounds, or a sidecar written by a newer version with a wider range,
|
||||||
/// must not produce out-of-range uniforms.
|
/// must not produce out-of-range uniforms.
|
||||||
pub fn clamp(&self, value: f32) -> f32 {
|
pub fn clamp(&self, value: f32) -> f32 {
|
||||||
match self.kind {
|
match &self.kind {
|
||||||
ParamKind::Scalar { min, max, .. } => {
|
ParamKind::Scalar { min, max, .. } => {
|
||||||
if value.is_finite() {
|
if value.is_finite() {
|
||||||
value.clamp(min, max)
|
value.clamp(*min, *max)
|
||||||
} else {
|
} else {
|
||||||
// A NaN from a corrupt sidecar would otherwise poison the
|
// A NaN from a corrupt sidecar would otherwise poison the
|
||||||
// uniform block and blank the image.
|
// uniform block and blank the image.
|
||||||
@@ -544,20 +632,45 @@ impl Attribute {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The static description of an operation.
|
/// TRACES: FR-PLG-2
|
||||||
|
/// The description of an operation.
|
||||||
|
///
|
||||||
|
/// # Owned, not `&'static`
|
||||||
|
///
|
||||||
|
/// This used to be a `static` with `&'static [ParamDescriptor]` inside it, and
|
||||||
|
/// [`crate::Operation::descriptor`] used to hand out a reference to it. That
|
||||||
|
/// shape made a build-time node free and a run-time node **impossible**: a
|
||||||
|
/// declaration parsed at startup has nothing to borrow from, so no amount of
|
||||||
|
/// interpreting `ops/*.yaml` at load time could ever produce a descriptor the
|
||||||
|
/// rest of the application would accept. FR-PLG-2 says a bundled operation and
|
||||||
|
/// a third-party plugin are the same kind of thing, differing only in where
|
||||||
|
/// the file was found — and a lifetime that only a compile-time literal can
|
||||||
|
/// satisfy is exactly a second, weaker format for outsiders.
|
||||||
|
///
|
||||||
|
/// So the descriptor owns its contents and is handed out as an
|
||||||
|
/// `Arc<OpDescriptor>`. The `Arc` rather than a `&self`-borrowed reference
|
||||||
|
/// because the callers want to *keep* it: the develop panel collects
|
||||||
|
/// descriptors and then mutates the graph, and a borrow would tie the
|
||||||
|
/// descriptor's lifetime to a borrow of the operation it came from — which is
|
||||||
|
/// the one thing `&'static` was doing right.
|
||||||
|
///
|
||||||
|
/// The cost is a refcount per read, on a path that reads descriptors when a
|
||||||
|
/// panel is built rather than per pixel. See `Operation::descriptor` for the
|
||||||
|
/// one place that is read per composition and why it does not matter.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct OpDescriptor {
|
pub struct OpDescriptor {
|
||||||
pub id: OpId,
|
pub id: OpId,
|
||||||
pub label: LocalizedKey,
|
pub label: LocalizedKey,
|
||||||
pub params: &'static [ParamDescriptor],
|
pub params: Vec<ParamDescriptor>,
|
||||||
/// What this operation is about (ARCH §4.3a).
|
/// What this operation is about (ARCH §4.3a).
|
||||||
///
|
///
|
||||||
/// **Never empty**, and `build.rs` refuses to generate an operation that
|
/// **Never empty**, and both the build-time and the load-time reader
|
||||||
/// declares none. An operation with no attribute would be invisible to a
|
/// refuse an operation that declares none. An operation with no attribute
|
||||||
/// frontend that filters by them, and a control that silently does not
|
/// would be invisible to a frontend that filters by them, and a control
|
||||||
/// exist is a worse failure than a build that stops — particularly when
|
/// that silently does not exist is a worse failure than a build that stops
|
||||||
/// the cause would be a missing line in a YAML file nobody looked at.
|
/// — particularly when the cause would be a missing line in a YAML file
|
||||||
pub attributes: &'static [Attribute],
|
/// nobody looked at.
|
||||||
|
pub attributes: Vec<Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpDescriptor {
|
impl OpDescriptor {
|
||||||
|
|||||||
@@ -519,7 +519,13 @@ pub fn compose_detail_with(
|
|||||||
) -> ComposedDetail {
|
) -> ComposedDetail {
|
||||||
// Every pass of every active detail operation, flattened, carrying the
|
// Every pass of every active detail operation, flattened, carrying the
|
||||||
// operation it came from for the uniform prefix and the helper set.
|
// operation it came from for the uniform prefix and the helper set.
|
||||||
let mut planned: Vec<(&'static str, &'static [Helper], DetailPass, usize)> = Vec::new();
|
//
|
||||||
|
// The helper slice borrows from the operation rather than being `'static`:
|
||||||
|
// `Operation::helpers` hands out a slice owned by the operation now, so
|
||||||
|
// that a node built from a declaration at load time can own its list
|
||||||
|
// (FR-PLG-2). The borrow lasts as long as `ops`, which outlives this
|
||||||
|
// function's body.
|
||||||
|
let mut planned: Vec<(&str, &[Helper], DetailPass, usize)> = Vec::new();
|
||||||
for (index, pass) in spots.iter().enumerate() {
|
for (index, pass) in spots.iter().enumerate() {
|
||||||
planned.push((
|
planned.push((
|
||||||
crate::spot::SPOT_ID,
|
crate::spot::SPOT_ID,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
//! was built for: a horizontal pass then a vertical one is mathematically a 2D
|
//! was built for: a horizontal pass then a vertical one is mathematically a 2D
|
||||||
//! box average, so if the ping-pong is wired backwards or a pass reads its own
|
//! box average, so if the ping-pong is wired backwards or a pass reads its own
|
||||||
//! output the result is visibly not a box blur rather than subtly wrong.
|
//! output the result is visibly not a box blur rather than subtly wrong.
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{
|
use crate::descriptor::{
|
||||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind, Scale, Unit,
|
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, ParamKind, Scale, Unit,
|
||||||
@@ -36,28 +37,30 @@ use crate::descriptor::{
|
|||||||
use crate::detail::{DetailPass, DetailStage, RenderScale};
|
use crate::detail::{DetailPass, DetailStage, RenderScale};
|
||||||
use crate::operation::{Affects, Operation, Uniform};
|
use crate::operation::{Affects, Operation, Uniform};
|
||||||
|
|
||||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
id: OpId("detail_probe"),
|
Arc::new(OpDescriptor {
|
||||||
label: LocalizedKey("op.detail_probe"),
|
id: OpId("detail_probe"),
|
||||||
params: &[ParamDescriptor {
|
label: LocalizedKey("op.detail_probe"),
|
||||||
id: ParamId("radius"),
|
params: vec![ParamDescriptor {
|
||||||
label: LocalizedKey("param.detail_probe.radius"),
|
id: ParamId("radius"),
|
||||||
// A fraction of the frame's shorter edge, which is the unit
|
label: LocalizedKey("param.detail_probe.radius"),
|
||||||
// `RenderScale::frame_fraction` converts and the unit a mask feather
|
// A fraction of the frame's shorter edge, which is the unit
|
||||||
// is already stored in. Stating it in pixels is the mistake this
|
// `RenderScale::frame_fraction` converts and the unit a mask feather
|
||||||
// whole stage is arranged to make impossible.
|
// is already stored in. Stating it in pixels is the mistake this
|
||||||
kind: ParamKind::Scalar {
|
// whole stage is arranged to make impossible.
|
||||||
min: 0.0,
|
kind: ParamKind::Scalar {
|
||||||
max: 0.25,
|
min: 0.0,
|
||||||
scale: Scale::Linear,
|
max: 0.25,
|
||||||
unit: Unit::None,
|
scale: Scale::Linear,
|
||||||
precision: 4,
|
unit: Unit::None,
|
||||||
},
|
precision: 4,
|
||||||
default: 0.0,
|
},
|
||||||
facet: None,
|
default: 0.0,
|
||||||
}],
|
facet: None,
|
||||||
attributes: &[Attribute::Detail],
|
}],
|
||||||
};
|
attributes: vec![Attribute::Detail],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
/// A separable box blur whose radius is a fraction of the frame's shorter edge.
|
/// A separable box blur whose radius is a fraction of the frame's shorter edge.
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
@@ -85,8 +88,8 @@ impl BoxBlur {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for BoxBlur {
|
impl Operation for BoxBlur {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
&DESCRIPTOR
|
DESCRIPTOR.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_param(&mut self, _id: ParamId, value: f32) {
|
fn set_param(&mut self, _id: ParamId, value: f32) {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
|
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{
|
use crate::descriptor::{
|
||||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation, Scale,
|
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation, Scale,
|
||||||
@@ -73,50 +74,52 @@ static FRAMING_PARAMS: [ParamId; 8] = [
|
|||||||
CROP_X, CROP_Y, CROP_W, CROP_H, ANGLE, ROTATION, FLIP_H, FLIP_V,
|
CROP_X, CROP_Y, CROP_W, CROP_H, ANGLE, ROTATION, FLIP_H, FLIP_V,
|
||||||
];
|
];
|
||||||
|
|
||||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
// The shape of the frame, and the only operation that changes the
|
Arc::new(OpDescriptor {
|
||||||
// output's dimensions.
|
// The shape of the frame, and the only operation that changes the
|
||||||
attributes: &[Attribute::Geometry],
|
// output's dimensions.
|
||||||
id: ID,
|
attributes: vec![Attribute::Geometry],
|
||||||
label: LocalizedKey("op.framing"),
|
id: ID,
|
||||||
params: &[
|
label: LocalizedKey("op.framing"),
|
||||||
// Straightening. Degrees rather than a normalised amount because a
|
params: vec![
|
||||||
// photographer reading "-1.4°" off a horizon knows what it means.
|
// Straightening. Degrees rather than a normalised amount because a
|
||||||
ParamDescriptor::scalar(
|
// photographer reading "-1.4°" off a horizon knows what it means.
|
||||||
"angle",
|
ParamDescriptor::scalar(
|
||||||
"param.angle",
|
"angle",
|
||||||
-MAX_STRAIGHTEN,
|
"param.angle",
|
||||||
MAX_STRAIGHTEN,
|
-MAX_STRAIGHTEN,
|
||||||
0.0,
|
MAX_STRAIGHTEN,
|
||||||
Unit::None,
|
0.0,
|
||||||
Scale::Linear,
|
Unit::None,
|
||||||
2,
|
Scale::Linear,
|
||||||
),
|
2,
|
||||||
// Quarter turns, 0..3. Separate from `angle` because these are exact
|
),
|
||||||
// and lossless, and because reorienting a frame is a different
|
// Quarter turns, 0..3. Separate from `angle` because these are exact
|
||||||
// gesture from nudging a horizon.
|
// and lossless, and because reorienting a frame is a different
|
||||||
ParamDescriptor::scalar(
|
// gesture from nudging a horizon.
|
||||||
"rotation",
|
ParamDescriptor::scalar(
|
||||||
"param.rotation",
|
"rotation",
|
||||||
0.0,
|
"param.rotation",
|
||||||
3.0,
|
0.0,
|
||||||
0.0,
|
3.0,
|
||||||
Unit::None,
|
0.0,
|
||||||
Scale::Linear,
|
Unit::None,
|
||||||
0,
|
Scale::Linear,
|
||||||
),
|
0,
|
||||||
ParamDescriptor::switch("flip_h", "param.flip_h"),
|
),
|
||||||
ParamDescriptor::switch("flip_v", "param.flip_v"),
|
ParamDescriptor::switch("flip_h", "param.flip_h"),
|
||||||
// The crop rect, in fractions of the source. Normalised rather than
|
ParamDescriptor::switch("flip_v", "param.flip_v"),
|
||||||
// in pixels so a crop survives being applied to a proxy, a full
|
// The crop rect, in fractions of the source. Normalised rather than
|
||||||
// resolution render, or an export at another size — the same reason
|
// in pixels so a crop survives being applied to a proxy, a full
|
||||||
// the viewport renders at display resolution (FR-DSP-1).
|
// resolution render, or an export at another size — the same reason
|
||||||
ParamDescriptor::fraction("crop_x", "param.crop_x", 0.0),
|
// the viewport renders at display resolution (FR-DSP-1).
|
||||||
ParamDescriptor::fraction("crop_y", "param.crop_y", 0.0),
|
ParamDescriptor::fraction("crop_x", "param.crop_x", 0.0),
|
||||||
ParamDescriptor::fraction("crop_w", "param.crop_w", 1.0),
|
ParamDescriptor::fraction("crop_y", "param.crop_y", 0.0),
|
||||||
ParamDescriptor::fraction("crop_h", "param.crop_h", 1.0),
|
ParamDescriptor::fraction("crop_w", "param.crop_w", 1.0),
|
||||||
],
|
ParamDescriptor::fraction("crop_h", "param.crop_h", 1.0),
|
||||||
};
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
/// A normalised crop rectangle, in fractions of the source image.
|
/// A normalised crop rectangle, in fractions of the source image.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
@@ -252,8 +255,8 @@ impl Framing {
|
|||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn descriptor(&self) -> &'static OpDescriptor {
|
pub fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
&DESCRIPTOR
|
DESCRIPTOR.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TRACES: FR-DEV-3a | FR-DEV-3b | FR-UI-7
|
/// TRACES: FR-DEV-3a | FR-DEV-3b | FR-UI-7
|
||||||
@@ -280,7 +283,7 @@ impl Framing {
|
|||||||
/// into the generated panel underneath a crop control that already exists.
|
/// into the generated panel underneath a crop control that already exists.
|
||||||
pub fn presentation(&self) -> Option<Presentation> {
|
pub fn presentation(&self) -> Option<Presentation> {
|
||||||
Some(Presentation {
|
Some(Presentation {
|
||||||
widgets: &[WidgetKind::CropOverlay],
|
widgets: vec![WidgetKind::CropOverlay],
|
||||||
demand: WidgetDemand {
|
demand: WidgetDemand {
|
||||||
// A crop rect is dragged by its corners; nothing about that
|
// A crop rect is dragged by its corners; nothing about that
|
||||||
// reduces to one axis at a time.
|
// reduces to one axis at a time.
|
||||||
@@ -290,7 +293,7 @@ impl Framing {
|
|||||||
// modality, so a thumb is as workable as a mouse.
|
// modality, so a thumb is as workable as a mouse.
|
||||||
precise_pointing: false,
|
precise_pointing: false,
|
||||||
},
|
},
|
||||||
params: &FRAMING_PARAMS,
|
params: FRAMING_PARAMS.to_vec(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1426,7 +1429,7 @@ mod tests {
|
|||||||
fn every_parameter_at_its_extremes_is_survivable() {
|
fn every_parameter_at_its_extremes_is_survivable() {
|
||||||
// The whole descriptor driven to both ends, which is what a codegen
|
// The whole descriptor driven to both ends, which is what a codegen
|
||||||
// test does and what a corrupt sidecar can do.
|
// test does and what a corrupt sidecar can do.
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
for value in [-1e9, -1.0, 0.0, 1.0, 1e9, f32::NAN] {
|
for value in [-1e9, -1.0, 0.0, 1.0, 1e9, f32::NAN] {
|
||||||
let mut f = Framing::new();
|
let mut f = Framing::new();
|
||||||
f.set_param(p.id, p.clamp(value));
|
f.set_param(p.id, p.clamp(value));
|
||||||
@@ -1610,7 +1613,7 @@ mod tests {
|
|||||||
// The same contract the operations honour, checked against the
|
// The same contract the operations honour, checked against the
|
||||||
// descriptor rather than a literal.
|
// descriptor rather than a literal.
|
||||||
let mut f = Framing::new();
|
let mut f = Framing::new();
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
f.set_param(p.id, p.default);
|
f.set_param(p.id, p.default);
|
||||||
}
|
}
|
||||||
assert!(!f.is_active(), "descriptor defaults must be neutral");
|
assert!(!f.is_active(), "descriptor defaults must be neutral");
|
||||||
@@ -1618,7 +1621,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_default_is_within_its_declared_range() {
|
fn every_default_is_within_its_declared_range() {
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
assert_eq!(p.clamp(p.default), p.default, "{} is out of range", p.id);
|
assert_eq!(p.clamp(p.default), p.default, "{} is out of range", p.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ pub struct OpCapability {
|
|||||||
/// other, so a new operation joins the right group by declaring what it
|
/// other, so a new operation joins the right group by declaring what it
|
||||||
/// is — which is the only thing its author is well placed to say.
|
/// is — which is the only thing its author is well placed to say.
|
||||||
///
|
///
|
||||||
/// Never empty; `build.rs` refuses an operation that declares none.
|
/// Never empty; both readers refuse an operation that declares none.
|
||||||
pub attributes: &'static [Attribute],
|
pub attributes: Vec<Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TRACES: FR-DEV-3a | FR-DEV-3b
|
/// TRACES: FR-DEV-3a | FR-DEV-3b
|
||||||
@@ -243,7 +243,7 @@ impl EditGraph {
|
|||||||
/// because this is what the codegen tests count `---- ` shader blocks
|
/// because this is what the codegen tests count `---- ` shader blocks
|
||||||
/// against, and framing generates a prologue rather than a colour block.
|
/// against, and framing generates a prologue rather than a colour block.
|
||||||
/// A UI wanting everything should read [`Self::capabilities`] (FR-DEV-3a).
|
/// A UI wanting everything should read [`Self::capabilities`] (FR-DEV-3a).
|
||||||
pub fn descriptors(&self) -> Vec<&'static OpDescriptor> {
|
pub fn descriptors(&self) -> Vec<Arc<OpDescriptor>> {
|
||||||
self.ops.iter().map(|o| o.descriptor()).collect()
|
self.ops.iter().map(|o| o.descriptor()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +281,7 @@ impl EditGraph {
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
presentation: op.presentation(),
|
presentation: op.presentation(),
|
||||||
attributes: desc.attributes,
|
attributes: desc.attributes.clone(),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -310,7 +310,7 @@ impl EditGraph {
|
|||||||
// sliders for framing *without naming framing* — see
|
// sliders for framing *without naming framing* — see
|
||||||
// `Framing::presentation`.
|
// `Framing::presentation`.
|
||||||
presentation: self.framing.presentation(),
|
presentation: self.framing.presentation(),
|
||||||
attributes: desc.attributes,
|
attributes: desc.attributes.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
ops.chain(std::iter::once(framing)).collect()
|
ops.chain(std::iter::once(framing)).collect()
|
||||||
@@ -427,7 +427,11 @@ impl EditGraph {
|
|||||||
|
|
||||||
pub fn set_param(&mut self, op: OpId, param: ParamId, value: f32) {
|
pub fn set_param(&mut self, op: OpId, param: ParamId, value: f32) {
|
||||||
if op == crate::framing::ID {
|
if op == crate::framing::ID {
|
||||||
let Some(desc) = self.framing.descriptor().param(param) else {
|
// Bound rather than chained: `descriptor()` hands back an owned
|
||||||
|
// `Arc` now, so a `param()` borrowed straight out of the call
|
||||||
|
// would outlive the temporary it came from.
|
||||||
|
let descriptor = self.framing.descriptor();
|
||||||
|
let Some(desc) = descriptor.param(param) else {
|
||||||
log::warn!("unknown parameter {param} on {op}; ignoring");
|
log::warn!("unknown parameter {param} on {op}; ignoring");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -469,7 +473,7 @@ impl EditGraph {
|
|||||||
/// Reset every parameter of every operation, and the framing, to default.
|
/// Reset every parameter of every operation, and the framing, to default.
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
for op in &mut self.ops {
|
for op in &mut self.ops {
|
||||||
for p in op.descriptor().params {
|
for p in &op.descriptor().params {
|
||||||
op.set_param(p.id, p.default);
|
op.set_param(p.id, p.default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -625,7 +629,7 @@ impl EditGraph {
|
|||||||
// structure key deliberately omits because they do not recompile a
|
// structure key deliberately omits because they do not recompile a
|
||||||
// shader. Both matter to a cached *result*, so both are here.
|
// shader. Both matter to a cached *result*, so both are here.
|
||||||
let mut geometry = mix(FNV_OFFSET, self.framing.structure_key());
|
let mut geometry = mix(FNV_OFFSET, self.framing.structure_key());
|
||||||
for p in self.framing.descriptor().params {
|
for p in &self.framing.descriptor().params {
|
||||||
geometry = hash_bytes(geometry, p.id.0.as_bytes());
|
geometry = hash_bytes(geometry, p.id.0.as_bytes());
|
||||||
geometry = mix(
|
geometry = mix(
|
||||||
geometry,
|
geometry,
|
||||||
@@ -944,7 +948,7 @@ mod tests {
|
|||||||
.capabilities()
|
.capabilities()
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|c| {
|
.flat_map(|c| {
|
||||||
c.params.iter().map(move |p| match p.kind {
|
c.params.iter().map(move |p| match &p.kind {
|
||||||
ParamKind::Scalar {
|
ParamKind::Scalar {
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
//! wearing the same lens, which defeats the point of a lens profile.
|
//! wearing the same lens, which defeats the point of a lens profile.
|
||||||
|
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::descriptor::{OpDescriptor, ParamId};
|
use crate::descriptor::{OpDescriptor, ParamId};
|
||||||
use crate::operation::{Helper, Uniform};
|
use crate::operation::{Helper, Uniform};
|
||||||
@@ -62,8 +63,10 @@ use crate::operation::{Helper, Uniform};
|
|||||||
/// Object-safe for the same reason [`crate::operation::Operation`] is: the
|
/// Object-safe for the same reason [`crate::operation::Operation`] is: the
|
||||||
/// graph holds `Box<dyn Warp>` in order, so the geometry chain is data.
|
/// graph holds `Box<dyn Warp>` in order, so the geometry chain is data.
|
||||||
pub trait Warp: Send + Sync {
|
pub trait Warp: Send + Sync {
|
||||||
/// Static description, driving UI generation exactly as for an operation.
|
/// This warp's description, driving UI generation exactly as for an
|
||||||
fn descriptor(&self) -> &'static OpDescriptor;
|
/// operation — including being owned rather than `&'static`, for the
|
||||||
|
/// reason [`crate::descriptor::OpDescriptor`] gives.
|
||||||
|
fn descriptor(&self) -> Arc<OpDescriptor>;
|
||||||
|
|
||||||
/// Set a parameter. Values arrive already clamped to the descriptor.
|
/// Set a parameter. Values arrive already clamped to the descriptor.
|
||||||
fn set_param(&mut self, id: ParamId, value: f32);
|
fn set_param(&mut self, id: ParamId, value: f32);
|
||||||
@@ -204,32 +207,38 @@ fn sanitise(id: &str) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::descriptor::Attribute;
|
use crate::descriptor::Attribute;
|
||||||
use crate::descriptor::{LocalizedKey, OpDescriptor, OpId, ParamDescriptor};
|
use crate::descriptor::{LocalizedKey, OpDescriptor, OpId, ParamDescriptor};
|
||||||
|
|
||||||
static DESC_A: OpDescriptor = OpDescriptor {
|
static DESC_A: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
id: OpId("warp_a"),
|
Arc::new(OpDescriptor {
|
||||||
label: LocalizedKey("a"),
|
id: OpId("warp_a"),
|
||||||
params: &[ParamDescriptor::amount("amount", "a.amount")],
|
label: LocalizedKey("a"),
|
||||||
attributes: &[Attribute::Tone],
|
params: vec![ParamDescriptor::amount("amount", "a.amount")],
|
||||||
};
|
attributes: vec![Attribute::Tone],
|
||||||
static DESC_B: OpDescriptor = OpDescriptor {
|
})
|
||||||
id: OpId("warp_b"),
|
});
|
||||||
label: LocalizedKey("b"),
|
static DESC_B: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
params: &[ParamDescriptor::amount("amount", "b.amount")],
|
Arc::new(OpDescriptor {
|
||||||
attributes: &[Attribute::Tone],
|
id: OpId("warp_b"),
|
||||||
};
|
label: LocalizedKey("b"),
|
||||||
|
params: vec![ParamDescriptor::amount("amount", "b.amount")],
|
||||||
|
attributes: vec![Attribute::Tone],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
struct Fake {
|
struct Fake {
|
||||||
desc: &'static OpDescriptor,
|
desc: Arc<OpDescriptor>,
|
||||||
amount: f32,
|
amount: f32,
|
||||||
splits: bool,
|
splits: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Warp for Fake {
|
impl Warp for Fake {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
self.desc
|
self.desc.clone()
|
||||||
}
|
}
|
||||||
fn set_param(&mut self, _id: ParamId, value: f32) {
|
fn set_param(&mut self, _id: ParamId, value: f32) {
|
||||||
self.amount = value;
|
self.amount = value;
|
||||||
@@ -254,7 +263,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fake(desc: &'static OpDescriptor, amount: f32, splits: bool) -> Box<dyn Warp> {
|
fn fake(desc: Arc<OpDescriptor>, amount: f32, splits: bool) -> Box<dyn Warp> {
|
||||||
Box::new(Fake {
|
Box::new(Fake {
|
||||||
desc,
|
desc,
|
||||||
amount,
|
amount,
|
||||||
@@ -266,7 +275,7 @@ mod tests {
|
|||||||
fn no_active_warp_composes_to_nothing() {
|
fn no_active_warp_composes_to_nothing() {
|
||||||
// The property that keeps the common case free: an image with no lens
|
// The property that keeps the common case free: an image with no lens
|
||||||
// correction must not pay for a bilinear sample.
|
// correction must not pay for a bilinear sample.
|
||||||
let composed = compose_warps(&[fake(&DESC_A, 0.0, false)]);
|
let composed = compose_warps(&[fake(DESC_A.clone(), 0.0, false)]);
|
||||||
assert!(!composed.is_active());
|
assert!(!composed.is_active());
|
||||||
assert!(composed.uniforms.is_empty());
|
assert!(composed.uniforms.is_empty());
|
||||||
assert!(!composed.splits_channels);
|
assert!(!composed.splits_channels);
|
||||||
@@ -274,7 +283,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_active_warp_appears_once() {
|
fn an_active_warp_appears_once() {
|
||||||
let composed = compose_warps(&[fake(&DESC_A, 2.0, false)]);
|
let composed = compose_warps(&[fake(DESC_A.clone(), 2.0, false)]);
|
||||||
assert!(composed.is_active());
|
assert!(composed.is_active());
|
||||||
assert!(composed.body.contains("---- warp: warp_a ----"));
|
assert!(composed.body.contains("---- warp: warp_a ----"));
|
||||||
assert!(composed.body.contains("u.warp_a_amount"));
|
assert!(composed.body.contains("u.warp_a_amount"));
|
||||||
@@ -284,7 +293,10 @@ mod tests {
|
|||||||
fn uniforms_are_prefixed_so_warps_cannot_collide() {
|
fn uniforms_are_prefixed_so_warps_cannot_collide() {
|
||||||
// Both fakes declare `amount`; without prefixing the generated struct
|
// Both fakes declare `amount`; without prefixing the generated struct
|
||||||
// would carry a duplicate field and fail to compile.
|
// would carry a duplicate field and fail to compile.
|
||||||
let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 2.0, false)]);
|
let composed = compose_warps(&[
|
||||||
|
fake(DESC_A.clone(), 1.0, false),
|
||||||
|
fake(DESC_B.clone(), 2.0, false),
|
||||||
|
]);
|
||||||
assert!(composed.uniform_fields.contains("warp_a_amount: f32"));
|
assert!(composed.uniform_fields.contains("warp_a_amount: f32"));
|
||||||
assert!(composed.uniform_fields.contains("warp_b_amount: f32"));
|
assert!(composed.uniform_fields.contains("warp_b_amount: f32"));
|
||||||
assert_eq!(composed.uniforms, vec![1.0, 2.0]);
|
assert_eq!(composed.uniforms, vec![1.0, 2.0]);
|
||||||
@@ -294,7 +306,10 @@ mod tests {
|
|||||||
fn channel_splitting_is_requested_by_any_active_warp() {
|
fn channel_splitting_is_requested_by_any_active_warp() {
|
||||||
// One CA warp among several must switch the whole stage to the
|
// One CA warp among several must switch the whole stage to the
|
||||||
// three-sample path.
|
// three-sample path.
|
||||||
let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, true)]);
|
let composed = compose_warps(&[
|
||||||
|
fake(DESC_A.clone(), 1.0, false),
|
||||||
|
fake(DESC_B.clone(), 1.0, true),
|
||||||
|
]);
|
||||||
assert!(composed.splits_channels);
|
assert!(composed.splits_channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,14 +317,20 @@ mod tests {
|
|||||||
fn an_inactive_splitting_warp_does_not_force_three_samples() {
|
fn an_inactive_splitting_warp_does_not_force_three_samples() {
|
||||||
// CA present but at neutral must cost nothing — otherwise every image
|
// CA present but at neutral must cost nothing — otherwise every image
|
||||||
// with the panel visible pays triple bandwidth.
|
// with the panel visible pays triple bandwidth.
|
||||||
let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 0.0, true)]);
|
let composed = compose_warps(&[
|
||||||
|
fake(DESC_A.clone(), 1.0, false),
|
||||||
|
fake(DESC_B.clone(), 0.0, true),
|
||||||
|
]);
|
||||||
assert!(composed.is_active());
|
assert!(composed.is_active());
|
||||||
assert!(!composed.splits_channels);
|
assert!(!composed.splits_channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn warps_compose_in_order() {
|
fn warps_compose_in_order() {
|
||||||
let composed = compose_warps(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, false)]);
|
let composed = compose_warps(&[
|
||||||
|
fake(DESC_A.clone(), 1.0, false),
|
||||||
|
fake(DESC_B.clone(), 1.0, false),
|
||||||
|
]);
|
||||||
let a = composed.body.find("warp_a").expect("a present");
|
let a = composed.body.find("warp_a").expect("a present");
|
||||||
let b = composed.body.find("warp_b").expect("b present");
|
let b = composed.body.find("warp_b").expect("b present");
|
||||||
assert!(a < b, "warps must chain in graph order");
|
assert!(a < b, "warps must chain in graph order");
|
||||||
|
|||||||
@@ -81,13 +81,13 @@ mod tests {
|
|||||||
let mut g = EditGraph::default_chain();
|
let mut g = EditGraph::default_chain();
|
||||||
for desc in g.descriptors() {
|
for desc in g.descriptors() {
|
||||||
for (i, p) in desc.params.iter().enumerate() {
|
for (i, p) in desc.params.iter().enumerate() {
|
||||||
let v = match p.kind {
|
let v = match &p.kind {
|
||||||
ParamKind::Scalar { min, max, .. } => {
|
ParamKind::Scalar { min, max, .. } => {
|
||||||
// A fraction that differs per parameter, so no two
|
// A fraction that differs per parameter, so no two
|
||||||
// move in lockstep.
|
// move in lockstep.
|
||||||
let fraction = 0.15 + 0.05 * (i % 4) as f32;
|
let fraction = 0.15 + 0.05 * (i % 4) as f32;
|
||||||
let step = (max - min) * fraction;
|
let step = (max - min) * fraction;
|
||||||
if p.default + step <= max {
|
if p.default + step <= *max {
|
||||||
p.default + step
|
p.default + step
|
||||||
} else {
|
} else {
|
||||||
p.default - step
|
p.default - step
|
||||||
@@ -217,7 +217,7 @@ mod tests {
|
|||||||
// A default outside its own range would mean a fresh image opens
|
// A default outside its own range would mean a fresh image opens
|
||||||
// with a value the UI cannot represent.
|
// with a value the UI cannot represent.
|
||||||
for desc in EditGraph::default_chain().descriptors() {
|
for desc in EditGraph::default_chain().descriptors() {
|
||||||
for p in desc.params {
|
for p in &desc.params {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
p.clamp(p.default),
|
p.clamp(p.default),
|
||||||
p.default,
|
p.default,
|
||||||
@@ -236,7 +236,7 @@ mod tests {
|
|||||||
// operation must read its own default as neutral.
|
// operation must read its own default as neutral.
|
||||||
let g = EditGraph::default_chain();
|
let g = EditGraph::default_chain();
|
||||||
for desc in g.descriptors() {
|
for desc in g.descriptors() {
|
||||||
for p in desc.params {
|
for p in &desc.params {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
g.param(desc.id, p.id),
|
g.param(desc.id, p.id),
|
||||||
Some(p.default),
|
Some(p.default),
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
//! see there for what happens when it does not match.
|
//! see there for what happens when it does not match.
|
||||||
|
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::descriptor::{OpDescriptor, ParamId};
|
use crate::descriptor::{OpDescriptor, ParamId};
|
||||||
use crate::operation::Operation;
|
use crate::operation::Operation;
|
||||||
@@ -692,7 +693,7 @@ impl Clone for MaskLayer {
|
|||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
let mut ops = layer_chain();
|
let mut ops = layer_chain();
|
||||||
for (dst, src) in ops.iter_mut().zip(&self.ops) {
|
for (dst, src) in ops.iter_mut().zip(&self.ops) {
|
||||||
for p in src.descriptor().params {
|
for p in &src.descriptor().params {
|
||||||
dst.set_param(p.id, src.param(p.id));
|
dst.set_param(p.id, src.param(p.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -947,7 +948,7 @@ impl MaskLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn descriptors(&self) -> Vec<&'static OpDescriptor> {
|
pub fn descriptors(&self) -> Vec<Arc<OpDescriptor>> {
|
||||||
self.ops.iter().map(|o| o.descriptor()).collect()
|
self.ops.iter().map(|o| o.descriptor()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,7 +996,7 @@ impl MaskLayer {
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
presentation: op.presentation(),
|
presentation: op.presentation(),
|
||||||
attributes: desc.attributes,
|
attributes: desc.attributes.clone(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -1007,7 +1008,7 @@ impl MaskLayer {
|
|||||||
/// arrive at — so "start this layer's edit again" must not throw it away.
|
/// arrive at — so "start this layer's edit again" must not throw it away.
|
||||||
pub fn reset_adjustments(&mut self) {
|
pub fn reset_adjustments(&mut self) {
|
||||||
for op in &mut self.ops {
|
for op in &mut self.ops {
|
||||||
for p in op.descriptor().params {
|
for p in &op.descriptor().params {
|
||||||
op.set_param(p.id, p.default);
|
op.set_param(p.id, p.default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1026,10 +1027,18 @@ impl MaskLayer {
|
|||||||
/// Every non-default parameter, for the sidecar.
|
/// Every non-default parameter, for the sidecar.
|
||||||
pub fn params(&self) -> impl Iterator<Item = (&'static str, &'static str, f32)> + '_ {
|
pub fn params(&self) -> impl Iterator<Item = (&'static str, &'static str, f32)> + '_ {
|
||||||
self.ops.iter().flat_map(|o| {
|
self.ops.iter().flat_map(|o| {
|
||||||
let id = o.descriptor().id.0;
|
let desc = o.descriptor();
|
||||||
o.descriptor().params.iter().filter_map(move |p| {
|
let id = desc.id.0;
|
||||||
let v = o.param(p.id);
|
// Collected rather than borrowed from `desc`: a descriptor is an
|
||||||
(v != p.default).then_some((id, p.id.0, v))
|
// `Arc` handed over by value now (FR-PLG-2), so it would be
|
||||||
|
// dropped at the end of this closure and the lazy iterator would
|
||||||
|
// outlive it. The ids and defaults are all this needs, and there
|
||||||
|
// are a handful of them.
|
||||||
|
let params: Vec<(ParamId, f32)> =
|
||||||
|
desc.params.iter().map(|p| (p.id, p.default)).collect();
|
||||||
|
params.into_iter().filter_map(move |(param, default)| {
|
||||||
|
let v = o.param(param);
|
||||||
|
(v != default).then_some((id, param.0, v))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
//! generator emits readable, commented output — see [`compose`].
|
//! generator emits readable, commented output — see [`compose`].
|
||||||
|
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use dr_types::{ColourSpace, Transfer};
|
use dr_types::{ColourSpace, Transfer};
|
||||||
|
|
||||||
@@ -171,7 +172,7 @@ impl Invalidation {
|
|||||||
pub(crate) fn hash_op(h: u64, op: &dyn Operation) -> u64 {
|
pub(crate) fn hash_op(h: u64, op: &dyn Operation) -> u64 {
|
||||||
let desc = op.descriptor();
|
let desc = op.descriptor();
|
||||||
let mut h = hash_bytes(h, desc.id.0.as_bytes());
|
let mut h = hash_bytes(h, desc.id.0.as_bytes());
|
||||||
for p in desc.params {
|
for p in &desc.params {
|
||||||
h = hash_bytes(h, p.id.0.as_bytes());
|
h = hash_bytes(h, p.id.0.as_bytes());
|
||||||
h = mix(h, u64::from(canonical_bits(op.param(p.id))));
|
h = mix(h, u64::from(canonical_bits(op.param(p.id))));
|
||||||
}
|
}
|
||||||
@@ -213,6 +214,12 @@ pub(crate) const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
|
|||||||
pub struct Uniform {
|
pub struct Uniform {
|
||||||
/// Field name as it appears in WGSL. Prefixed with the op id by the
|
/// Field name as it appears in WGSL. Prefixed with the op id by the
|
||||||
/// composer, so two operations may both declare `amount`.
|
/// composer, so two operations may both declare `amount`.
|
||||||
|
///
|
||||||
|
/// `&'static str` for the reason [`crate::descriptor::intern`] gives about
|
||||||
|
/// ids: a uniform name is a small, deduplicated, process-lifetime piece of
|
||||||
|
/// vocabulary, and a declared operation interns its names once when it is
|
||||||
|
/// parsed rather than allocating them on every `uniforms()` call — which
|
||||||
|
/// happens per composition, and composition happens per frame.
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub value: f32,
|
pub value: f32,
|
||||||
}
|
}
|
||||||
@@ -222,8 +229,32 @@ pub struct Uniform {
|
|||||||
/// Object-safe: the pipeline holds `Box<dyn Operation>` in graph order, so
|
/// Object-safe: the pipeline holds `Box<dyn Operation>` in graph order, so
|
||||||
/// order is data rather than code (ARCH §3.4).
|
/// order is data rather than code (ARCH §3.4).
|
||||||
pub trait Operation: Send + Sync {
|
pub trait Operation: Send + Sync {
|
||||||
/// Static description, driving UI generation (FR-DEV-3a).
|
/// TRACES: FR-DEV-3a | FR-PLG-2
|
||||||
fn descriptor(&self) -> &'static OpDescriptor;
|
/// This operation's description, driving UI generation (FR-DEV-3a).
|
||||||
|
///
|
||||||
|
/// **Shared and owned rather than `&'static`.** See [`OpDescriptor`] for
|
||||||
|
/// why — in short, a `&'static` descriptor is one a compile-time literal
|
||||||
|
/// can produce and a load-time declaration cannot, which would make a
|
||||||
|
/// plugin a second-class kind of operation for a reason that is purely an
|
||||||
|
/// artefact of how the built-ins happen to be written.
|
||||||
|
///
|
||||||
|
/// # What this costs, and where
|
||||||
|
///
|
||||||
|
/// One `Arc` clone and drop per call. Descriptors are read when a panel is
|
||||||
|
/// built (`EditGraph::capabilities`), when a sidecar is written or read,
|
||||||
|
/// and when the history names what changed — none of which is a per-frame
|
||||||
|
/// path.
|
||||||
|
///
|
||||||
|
/// There is **one** exception, and it is worth stating plainly rather than
|
||||||
|
/// letting somebody discover it with a profiler: [`compose_full`] reads
|
||||||
|
/// `descriptor().id` once per *active* operation to prefix its uniforms,
|
||||||
|
/// and `dr-ui` composes on every frame it draws. That is a handful of
|
||||||
|
/// atomic increments — a dozen or so, against a composition that is
|
||||||
|
/// already building several kilobytes of WGSL text from scratch on the
|
||||||
|
/// same call. If composition ever stops being a per-frame operation, this
|
||||||
|
/// stops being a question at all; while it is one, the refcount is not
|
||||||
|
/// what makes it expensive.
|
||||||
|
fn descriptor(&self) -> Arc<OpDescriptor>;
|
||||||
|
|
||||||
/// Set a parameter. Values arrive already clamped to the descriptor.
|
/// Set a parameter. Values arrive already clamped to the descriptor.
|
||||||
fn set_param(&mut self, id: ParamId, value: f32);
|
fn set_param(&mut self, id: ParamId, value: f32);
|
||||||
@@ -321,7 +352,13 @@ pub trait Operation: Send + Sync {
|
|||||||
/// Emitted once per *distinct* function name even if several operations
|
/// Emitted once per *distinct* function name even if several operations
|
||||||
/// request it, so shared helpers (luminance, soft clipping) are declared
|
/// request it, so shared helpers (luminance, soft clipping) are declared
|
||||||
/// exactly once.
|
/// exactly once.
|
||||||
fn helpers(&self) -> &'static [Helper] {
|
///
|
||||||
|
/// Borrowed from `self` rather than `'static`, for the reason
|
||||||
|
/// [`Self::descriptor`] is owned: a generated operation returns a
|
||||||
|
/// `&'static [Helper]` and coerces, while an operation built from a
|
||||||
|
/// declaration at load time owns its list. The [`Helper`] *strings*
|
||||||
|
/// themselves stay `&'static` — they are interned, like the ids.
|
||||||
|
fn helpers(&self) -> &[Helper] {
|
||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1184,31 +1221,37 @@ pub(crate) fn sanitise(id: &str) -> String {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use crate::descriptor::Attribute;
|
use crate::descriptor::Attribute;
|
||||||
use crate::descriptor::{LocalizedKey, OpId, ParamDescriptor};
|
use crate::descriptor::{LocalizedKey, OpId, ParamDescriptor};
|
||||||
|
|
||||||
static DESC_A: OpDescriptor = OpDescriptor {
|
static DESC_A: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
id: OpId("op_a"),
|
Arc::new(OpDescriptor {
|
||||||
label: LocalizedKey("a"),
|
id: OpId("op_a"),
|
||||||
params: &[ParamDescriptor::amount("amount", "a.amount")],
|
label: LocalizedKey("a"),
|
||||||
attributes: &[Attribute::Tone],
|
params: vec![ParamDescriptor::amount("amount", "a.amount")],
|
||||||
};
|
attributes: vec![Attribute::Tone],
|
||||||
static DESC_B: OpDescriptor = OpDescriptor {
|
})
|
||||||
id: OpId("op_b"),
|
});
|
||||||
label: LocalizedKey("b"),
|
static DESC_B: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
params: &[ParamDescriptor::amount("amount", "b.amount")],
|
Arc::new(OpDescriptor {
|
||||||
attributes: &[Attribute::Tone],
|
id: OpId("op_b"),
|
||||||
};
|
label: LocalizedKey("b"),
|
||||||
|
params: vec![ParamDescriptor::amount("amount", "b.amount")],
|
||||||
|
attributes: vec![Attribute::Tone],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
struct Fake {
|
struct Fake {
|
||||||
desc: &'static OpDescriptor,
|
desc: Arc<OpDescriptor>,
|
||||||
amount: f32,
|
amount: f32,
|
||||||
helper: Option<Helper>,
|
helper: Option<Helper>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for Fake {
|
impl Operation for Fake {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
self.desc
|
self.desc.clone()
|
||||||
}
|
}
|
||||||
fn set_param(&mut self, _id: ParamId, value: f32) {
|
fn set_param(&mut self, _id: ParamId, value: f32) {
|
||||||
self.amount = value;
|
self.amount = value;
|
||||||
@@ -1241,7 +1284,7 @@ mod tests {
|
|||||||
source: "fn luma(c: vec3<f32>) -> f32 { return c.g; }",
|
source: "fn luma(c: vec3<f32>) -> f32 { return c.g; }",
|
||||||
}];
|
}];
|
||||||
|
|
||||||
fn fake(desc: &'static OpDescriptor, amount: f32, helper: bool) -> Box<dyn Operation> {
|
fn fake(desc: Arc<OpDescriptor>, amount: f32, helper: bool) -> Box<dyn Operation> {
|
||||||
Box::new(Fake {
|
Box::new(Fake {
|
||||||
desc,
|
desc,
|
||||||
amount,
|
amount,
|
||||||
@@ -1253,7 +1296,7 @@ mod tests {
|
|||||||
fn an_inactive_operation_contributes_nothing() {
|
fn an_inactive_operation_contributes_nothing() {
|
||||||
// The point of composing rather than branching: an op at neutral
|
// The point of composing rather than branching: an op at neutral
|
||||||
// must not appear in the source at all.
|
// must not appear in the source at all.
|
||||||
let ops = vec![fake(&DESC_A, 0.0, false)];
|
let ops = vec![fake(DESC_A.clone(), 0.0, false)];
|
||||||
let shader = compose(&ops);
|
let shader = compose(&ops);
|
||||||
assert!(
|
assert!(
|
||||||
!shader.source.contains("op_a"),
|
!shader.source.contains("op_a"),
|
||||||
@@ -1274,7 +1317,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn an_active_operation_appears_once() {
|
fn an_active_operation_appears_once() {
|
||||||
let ops = vec![fake(&DESC_A, 2.0, false)];
|
let ops = vec![fake(DESC_A.clone(), 2.0, false)];
|
||||||
let shader = compose(&ops);
|
let shader = compose(&ops);
|
||||||
assert!(shader.source.contains("---- op_a ----"));
|
assert!(shader.source.contains("---- op_a ----"));
|
||||||
assert!(shader.source.contains("u.op_a_amount"));
|
assert!(shader.source.contains("u.op_a_amount"));
|
||||||
@@ -1285,7 +1328,10 @@ mod tests {
|
|||||||
// Both fakes declare a uniform called `amount`. Without prefixing,
|
// Both fakes declare a uniform called `amount`. Without prefixing,
|
||||||
// the generated struct would have a duplicate field and fail to
|
// the generated struct would have a duplicate field and fail to
|
||||||
// compile — the failure mode that makes naive concatenation fragile.
|
// compile — the failure mode that makes naive concatenation fragile.
|
||||||
let ops = vec![fake(&DESC_A, 1.0, false), fake(&DESC_B, 2.0, false)];
|
let ops = vec![
|
||||||
|
fake(DESC_A.clone(), 1.0, false),
|
||||||
|
fake(DESC_B.clone(), 2.0, false),
|
||||||
|
];
|
||||||
let shader = compose(&ops);
|
let shader = compose(&ops);
|
||||||
assert!(shader.source.contains("op_a_amount: f32"));
|
assert!(shader.source.contains("op_a_amount: f32"));
|
||||||
assert!(shader.source.contains("op_b_amount: f32"));
|
assert!(shader.source.contains("op_b_amount: f32"));
|
||||||
@@ -1295,7 +1341,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn uniform_values_follow_declaration_order() {
|
fn uniform_values_follow_declaration_order() {
|
||||||
let ops = vec![fake(&DESC_A, 1.5, false), fake(&DESC_B, 2.5, false)];
|
let ops = vec![
|
||||||
|
fake(DESC_A.clone(), 1.5, false),
|
||||||
|
fake(DESC_B.clone(), 2.5, false),
|
||||||
|
];
|
||||||
let shader = compose(&ops);
|
let shader = compose(&ops);
|
||||||
assert_eq!(shader.uniforms[PREAMBLE_FIELDS], 1.5);
|
assert_eq!(shader.uniforms[PREAMBLE_FIELDS], 1.5);
|
||||||
assert_eq!(shader.uniforms[PREAMBLE_FIELDS + 1], 2.5);
|
assert_eq!(shader.uniforms[PREAMBLE_FIELDS + 1], 2.5);
|
||||||
@@ -1305,7 +1354,10 @@ mod tests {
|
|||||||
fn a_shared_helper_is_emitted_once() {
|
fn a_shared_helper_is_emitted_once() {
|
||||||
// Two operations wanting the same helper must not produce a
|
// Two operations wanting the same helper must not produce a
|
||||||
// duplicate function definition.
|
// duplicate function definition.
|
||||||
let ops = vec![fake(&DESC_A, 1.0, true), fake(&DESC_B, 1.0, true)];
|
let ops = vec![
|
||||||
|
fake(DESC_A.clone(), 1.0, true),
|
||||||
|
fake(DESC_B.clone(), 1.0, true),
|
||||||
|
];
|
||||||
let shader = compose(&ops);
|
let shader = compose(&ops);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
shader.source.matches("fn luma(").count(),
|
shader.source.matches("fn luma(").count(),
|
||||||
@@ -1319,7 +1371,17 @@ mod tests {
|
|||||||
// WGSL rejects a uniform struct whose size is not a multiple of 16.
|
// WGSL rejects a uniform struct whose size is not a multiple of 16.
|
||||||
for n in 0..6 {
|
for n in 0..6 {
|
||||||
let ops: Vec<Box<dyn Operation>> = (0..n)
|
let ops: Vec<Box<dyn Operation>> = (0..n)
|
||||||
.map(|i| fake(if i % 2 == 0 { &DESC_A } else { &DESC_B }, 1.0, false))
|
.map(|i| {
|
||||||
|
fake(
|
||||||
|
if i % 2 == 0 {
|
||||||
|
DESC_A.clone()
|
||||||
|
} else {
|
||||||
|
DESC_B.clone()
|
||||||
|
},
|
||||||
|
1.0,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let shader = compose(&ops);
|
let shader = compose(&ops);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1335,11 +1397,14 @@ mod tests {
|
|||||||
fn structure_hash_ignores_values_but_tracks_the_op_set() {
|
fn structure_hash_ignores_values_but_tracks_the_op_set() {
|
||||||
// The property the shader cache depends on: moving a slider must not
|
// The property the shader cache depends on: moving a slider must not
|
||||||
// trigger a recompile, but enabling an operation must.
|
// trigger a recompile, but enabling an operation must.
|
||||||
let a1 = compose(&[fake(&DESC_A, 1.0, false)]).structure_hash;
|
let a1 = compose(&[fake(DESC_A.clone(), 1.0, false)]).structure_hash;
|
||||||
let a2 = compose(&[fake(&DESC_A, 9.0, false)]).structure_hash;
|
let a2 = compose(&[fake(DESC_A.clone(), 9.0, false)]).structure_hash;
|
||||||
assert_eq!(a1, a2, "a value change must reuse the compiled pipeline");
|
assert_eq!(a1, a2, "a value change must reuse the compiled pipeline");
|
||||||
|
|
||||||
let both = compose(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, false)]);
|
let both = compose(&[
|
||||||
|
fake(DESC_A.clone(), 1.0, false),
|
||||||
|
fake(DESC_B.clone(), 1.0, false),
|
||||||
|
]);
|
||||||
assert_ne!(a1, both.structure_hash, "a different op-set must recompile");
|
assert_ne!(a1, both.structure_hash, "a different op-set must recompile");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1347,8 +1412,14 @@ mod tests {
|
|||||||
fn structure_hash_is_order_sensitive() {
|
fn structure_hash_is_order_sensitive() {
|
||||||
// Operation order is data (ARCH §3.4); two orders are different
|
// Operation order is data (ARCH §3.4); two orders are different
|
||||||
// shaders and must not share a cache entry.
|
// shaders and must not share a cache entry.
|
||||||
let ab = compose(&[fake(&DESC_A, 1.0, false), fake(&DESC_B, 1.0, false)]);
|
let ab = compose(&[
|
||||||
let ba = compose(&[fake(&DESC_B, 1.0, false), fake(&DESC_A, 1.0, false)]);
|
fake(DESC_A.clone(), 1.0, false),
|
||||||
|
fake(DESC_B.clone(), 1.0, false),
|
||||||
|
]);
|
||||||
|
let ba = compose(&[
|
||||||
|
fake(DESC_B.clone(), 1.0, false),
|
||||||
|
fake(DESC_A.clone(), 1.0, false),
|
||||||
|
]);
|
||||||
assert_ne!(ab.structure_hash, ba.structure_hash);
|
assert_ne!(ab.structure_hash, ba.structure_hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1412,7 +1483,7 @@ mod tests {
|
|||||||
// Exposure and the tonal controls act on white-balanced values; if
|
// Exposure and the tonal controls act on white-balanced values; if
|
||||||
// the multiply came afterwards, every operation would be reasoning
|
// the multiply came afterwards, every operation would be reasoning
|
||||||
// about a green-cast image.
|
// about a green-cast image.
|
||||||
let ops = vec![fake(&DESC_A, 2.0, false)];
|
let ops = vec![fake(DESC_A.clone(), 2.0, false)];
|
||||||
let source = compose(&ops).source;
|
let source = compose(&ops).source;
|
||||||
let wb = source.find("u.as_shot_wb").expect("wb applied");
|
let wb = source.find("u.as_shot_wb").expect("wb applied");
|
||||||
let op = source.find("---- op_a ----").expect("op present");
|
let op = source.find("---- op_a ----").expect("op present");
|
||||||
@@ -1472,7 +1543,7 @@ mod tests {
|
|||||||
// The other half, and the one that would fail silently: a bug that
|
// The other half, and the one that would fail silently: a bug that
|
||||||
// suppressed the tail unconditionally renders every ordinary edit
|
// suppressed the tail unconditionally renders every ordinary edit
|
||||||
// flat and uncorrected, which reads as a broken camera profile.
|
// flat and uncorrected, which reads as a broken camera profile.
|
||||||
let source = compose(&[fake(&DESC_A, 2.0, false)]).source;
|
let source = compose(&[fake(DESC_A.clone(), 2.0, false)]).source;
|
||||||
assert!(source.contains("base_curve_last.z > 0.5"));
|
assert!(source.contains("base_curve_last.z > 0.5"));
|
||||||
assert!(source.contains("Camera space -> linear sRGB"));
|
assert!(source.contains("Camera space -> linear sRGB"));
|
||||||
}
|
}
|
||||||
@@ -1484,7 +1555,7 @@ mod tests {
|
|||||||
// stock loaded is the default state of every photograph in the
|
// stock loaded is the default state of every photograph in the
|
||||||
// catalogue, and it must not disturb the camera's own rendering.
|
// catalogue, and it must not disturb the camera's own rendering.
|
||||||
let film: Box<dyn Operation> = Box::new(crate::ops::FilmSim::new());
|
let film: Box<dyn Operation> = Box::new(crate::ops::FilmSim::new());
|
||||||
let source = compose(&[film, fake(&DESC_A, 2.0, false)]).source;
|
let source = compose(&[film, fake(DESC_A.clone(), 2.0, false)]).source;
|
||||||
assert!(source.contains("base_curve_last.z > 0.5"));
|
assert!(source.contains("base_curve_last.z > 0.5"));
|
||||||
assert!(source.contains("Camera space -> linear sRGB"));
|
assert!(source.contains("Camera space -> linear sRGB"));
|
||||||
}
|
}
|
||||||
@@ -1493,7 +1564,7 @@ mod tests {
|
|||||||
fn the_camera_matrix_is_applied_after_the_operations() {
|
fn the_camera_matrix_is_applied_after_the_operations() {
|
||||||
// Adjustments are meaningful in sensor-native space, where highlight
|
// Adjustments are meaningful in sensor-native space, where highlight
|
||||||
// headroom still exists; converting first would clip it away.
|
// headroom still exists; converting first would clip it away.
|
||||||
let ops = vec![fake(&DESC_A, 2.0, false)];
|
let ops = vec![fake(DESC_A.clone(), 2.0, false)];
|
||||||
let source = compose(&ops).source;
|
let source = compose(&ops).source;
|
||||||
let op = source.find("---- op_a ----").expect("op present");
|
let op = source.find("---- op_a ----").expect("op present");
|
||||||
let matrix = source.find("u.cam_to_srgb_0").expect("matrix applied");
|
let matrix = source.find("u.cam_to_srgb_0").expect("matrix applied");
|
||||||
@@ -1514,7 +1585,7 @@ mod tests {
|
|||||||
// Before the matrix: the curve was tuned against this body's own
|
// Before the matrix: the curve was tuned against this body's own
|
||||||
// primaries. Applied after the conversion it would be a Canon
|
// primaries. Applied after the conversion it would be a Canon
|
||||||
// rendering acting on sRGB values, which is a different curve.
|
// rendering acting on sRGB values, which is a different curve.
|
||||||
let ops = vec![fake(&DESC_A, 2.0, false)];
|
let ops = vec![fake(DESC_A.clone(), 2.0, false)];
|
||||||
let source = compose(&ops).source;
|
let source = compose(&ops).source;
|
||||||
let op = source.find("---- op_a ----").expect("op present");
|
let op = source.find("---- op_a ----").expect("op present");
|
||||||
let curve = source
|
let curve = source
|
||||||
@@ -1592,7 +1663,7 @@ mod tests {
|
|||||||
// it must not pick up an identity matrix multiply for the sake of
|
// it must not pick up an identity matrix multiply for the sake of
|
||||||
// generality. Asserted against the source rather than against timing,
|
// generality. Asserted against the source rather than against timing,
|
||||||
// which would not fail reliably.
|
// which would not fail reliably.
|
||||||
let ops = vec![fake(&DESC_A, 1.0, false)];
|
let ops = vec![fake(DESC_A.clone(), 1.0, false)];
|
||||||
let srgb = compose_to(&ops, ColourSpace::Srgb).source;
|
let srgb = compose_to(&ops, ColourSpace::Srgb).source;
|
||||||
assert!(
|
assert!(
|
||||||
!srgb.contains("Linear sRGB -> linear sRGB"),
|
!srgb.contains("Linear sRGB -> linear sRGB"),
|
||||||
@@ -1607,7 +1678,7 @@ mod tests {
|
|||||||
// in linear sRGB, the primaries conversion carries it into the wider
|
// in linear sRGB, the primaries conversion carries it into the wider
|
||||||
// space, and only then is it clipped — clipping first would discard
|
// space, and only then is it clipped — clipping first would discard
|
||||||
// exactly the colours the wider space was chosen to keep.
|
// exactly the colours the wider space was chosen to keep.
|
||||||
let source = compose_to(&[fake(&DESC_A, 1.0, false)], ColourSpace::DisplayP3).source;
|
let source = compose_to(&[fake(DESC_A.clone(), 1.0, false)], ColourSpace::DisplayP3).source;
|
||||||
let camera = source.find("u.cam_to_srgb_0").expect("camera matrix");
|
let camera = source.find("u.cam_to_srgb_0").expect("camera matrix");
|
||||||
let convert = source
|
let convert = source
|
||||||
.find("Linear sRGB -> linear Display P3")
|
.find("Linear sRGB -> linear Display P3")
|
||||||
@@ -1659,7 +1730,7 @@ mod tests {
|
|||||||
// an export that came out sRGB and claimed to be Display P3.
|
// an export that came out sRGB and claimed to be Display P3.
|
||||||
let mut seen: Vec<u64> = Vec::new();
|
let mut seen: Vec<u64> = Vec::new();
|
||||||
for space in ColourSpace::ALL {
|
for space in ColourSpace::ALL {
|
||||||
let h = compose_to(&[fake(&DESC_A, 1.0, false)], space).structure_hash;
|
let h = compose_to(&[fake(DESC_A.clone(), 1.0, false)], space).structure_hash;
|
||||||
assert!(!seen.contains(&h), "{space:?} collides with another space");
|
assert!(!seen.contains(&h), "{space:?} collides with another space");
|
||||||
seen.push(h);
|
seen.push(h);
|
||||||
}
|
}
|
||||||
@@ -1669,7 +1740,7 @@ mod tests {
|
|||||||
fn generated_source_carries_a_do_not_edit_banner() {
|
fn generated_source_carries_a_do_not_edit_banner() {
|
||||||
// Someone will eventually find this in a debugger and try to fix it
|
// Someone will eventually find this in a debugger and try to fix it
|
||||||
// in place.
|
// in place.
|
||||||
let shader = compose(&[fake(&DESC_A, 1.0, false)]);
|
let shader = compose(&[fake(DESC_A.clone(), 1.0, false)]);
|
||||||
assert!(shader.source.starts_with("// GENERATED"));
|
assert!(shader.source.starts_with("// GENERATED"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
//! untouched means a mis-set correction shifts the channels that contribute
|
//! untouched means a mis-set correction shifts the channels that contribute
|
||||||
//! least to perceived sharpness. Scaling all three about a virtual reference
|
//! least to perceived sharpness. Scaling all three about a virtual reference
|
||||||
//! would soften the image even when the correction is right.
|
//! would soften the image even when the correction is right.
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{
|
use crate::descriptor::{
|
||||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
||||||
@@ -49,36 +50,38 @@ pub const BLUE: ParamId = ParamId("blue");
|
|||||||
/// resolution left to tune by eye at 100%.
|
/// resolution left to tune by eye at 100%.
|
||||||
const MAX_SCALE: f32 = 0.005;
|
const MAX_SCALE: f32 = 0.005;
|
||||||
|
|
||||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
attributes: &[Attribute::Optics],
|
Arc::new(OpDescriptor {
|
||||||
id: ID,
|
attributes: vec![Attribute::Optics],
|
||||||
label: LocalizedKey("op.aberration"),
|
id: ID,
|
||||||
params: &[
|
label: LocalizedKey("op.aberration"),
|
||||||
// Two independent controls rather than one: the red and blue
|
params: vec![
|
||||||
// displacements are caused by different ends of the spectrum and are
|
// Two independent controls rather than one: the red and blue
|
||||||
// not symmetric, so a single "fringing" slider could not remove both.
|
// displacements are caused by different ends of the spectrum and are
|
||||||
ParamDescriptor::scalar(
|
// not symmetric, so a single "fringing" slider could not remove both.
|
||||||
"red",
|
ParamDescriptor::scalar(
|
||||||
"param.aberration.red",
|
"red",
|
||||||
-100.0,
|
"param.aberration.red",
|
||||||
100.0,
|
-100.0,
|
||||||
0.0,
|
100.0,
|
||||||
Unit::None,
|
0.0,
|
||||||
Scale::Linear,
|
Unit::None,
|
||||||
0,
|
Scale::Linear,
|
||||||
),
|
0,
|
||||||
ParamDescriptor::scalar(
|
),
|
||||||
"blue",
|
ParamDescriptor::scalar(
|
||||||
"param.aberration.blue",
|
"blue",
|
||||||
-100.0,
|
"param.aberration.blue",
|
||||||
100.0,
|
-100.0,
|
||||||
0.0,
|
100.0,
|
||||||
Unit::None,
|
0.0,
|
||||||
Scale::Linear,
|
Unit::None,
|
||||||
0,
|
Scale::Linear,
|
||||||
),
|
0,
|
||||||
],
|
),
|
||||||
};
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct Aberration {
|
pub struct Aberration {
|
||||||
@@ -113,8 +116,8 @@ impl Aberration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Warp for Aberration {
|
impl Warp for Aberration {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
&DESCRIPTOR
|
DESCRIPTOR.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||||
@@ -300,7 +303,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn every_default_is_neutral() {
|
fn every_default_is_neutral() {
|
||||||
let mut a = Aberration::new();
|
let mut a = Aberration::new();
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
a.set_param(p.id, p.default);
|
a.set_param(p.id, p.default);
|
||||||
}
|
}
|
||||||
assert!(!a.is_active(), "descriptor defaults must be neutral");
|
assert!(!a.is_active(), "descriptor defaults must be neutral");
|
||||||
|
|||||||
@@ -124,6 +124,7 @@
|
|||||||
//! would be a guess dressed as a number; `resolves` is the line the stage
|
//! would be a guess dressed as a number; `resolves` is the line the stage
|
||||||
//! already draws, and drawing it in two places differently is worse than a
|
//! already draws, and drawing it in two places differently is worse than a
|
||||||
//! visible step.
|
//! visible step.
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{
|
use crate::descriptor::{
|
||||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
||||||
@@ -163,46 +164,48 @@ const MAX_KERNEL: f32 = 48.0;
|
|||||||
/// every editor's capture sharpening starts.
|
/// every editor's capture sharpening starts.
|
||||||
const DEFAULT_RADIUS: f32 = 1.0;
|
const DEFAULT_RADIUS: f32 = 1.0;
|
||||||
|
|
||||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
id: ID,
|
Arc::new(OpDescriptor {
|
||||||
label: LocalizedKey("op.capture_sharpen"),
|
id: ID,
|
||||||
params: &[
|
label: LocalizedKey("op.capture_sharpen"),
|
||||||
// Amount carries the neutral, which is why it is first: the operation
|
params: vec![
|
||||||
// is off when this is zero regardless of the other two, so a reset is
|
// Amount carries the neutral, which is why it is first: the operation
|
||||||
// one control and the panel's ordering matches the way it is used.
|
// is off when this is zero regardless of the other two, so a reset is
|
||||||
ParamDescriptor::amount("amount", "param.amount"),
|
// one control and the panel's ordering matches the way it is used.
|
||||||
// In **source pixels** — see the module documentation. Half a photosite
|
ParamDescriptor::amount("amount", "param.amount"),
|
||||||
// is the smallest radius that means anything on a Bayer sensor, and
|
// In **source pixels** — see the module documentation. Half a photosite
|
||||||
// three is already past the point where an unsharp mask is sharpening
|
// is the smallest radius that means anything on a Bayer sensor, and
|
||||||
// rather than adding local contrast; a photographer wanting the latter
|
// three is already past the point where an unsharp mask is sharpening
|
||||||
// wants clarity, which is a different operation with a different unit.
|
// rather than adding local contrast; a photographer wanting the latter
|
||||||
ParamDescriptor::scalar(
|
// wants clarity, which is a different operation with a different unit.
|
||||||
"radius",
|
ParamDescriptor::scalar(
|
||||||
"param.radius",
|
"radius",
|
||||||
0.5,
|
"param.radius",
|
||||||
3.0,
|
0.5,
|
||||||
DEFAULT_RADIUS,
|
3.0,
|
||||||
Unit::None,
|
DEFAULT_RADIUS,
|
||||||
Scale::Linear,
|
Unit::None,
|
||||||
2,
|
Scale::Linear,
|
||||||
),
|
2,
|
||||||
// A fraction, but declared as a scalar rather than through
|
),
|
||||||
// `ParamDescriptor::fraction` for its precision alone: four decimal
|
// A fraction, but declared as a scalar rather than through
|
||||||
// places on a control whose whole useful travel is a dozen steps
|
// `ParamDescriptor::fraction` for its precision alone: four decimal
|
||||||
// reads as noise, and invites fiddling with digits that do nothing.
|
// places on a control whose whole useful travel is a dozen steps
|
||||||
ParamDescriptor::scalar(
|
// reads as noise, and invites fiddling with digits that do nothing.
|
||||||
"threshold",
|
ParamDescriptor::scalar(
|
||||||
"param.threshold",
|
"threshold",
|
||||||
0.0,
|
"param.threshold",
|
||||||
1.0,
|
0.0,
|
||||||
0.0,
|
1.0,
|
||||||
Unit::None,
|
0.0,
|
||||||
Scale::Linear,
|
Unit::None,
|
||||||
2,
|
Scale::Linear,
|
||||||
),
|
2,
|
||||||
],
|
),
|
||||||
attributes: &[Attribute::Detail],
|
],
|
||||||
};
|
attributes: vec![Attribute::Detail],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
/// TRACES: FR-DEV-3
|
/// TRACES: FR-DEV-3
|
||||||
/// Capture sharpening: a separable unsharp mask with a contrast threshold.
|
/// Capture sharpening: a separable unsharp mask with a contrast threshold.
|
||||||
@@ -275,8 +278,8 @@ impl CaptureSharpen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for CaptureSharpen {
|
impl Operation for CaptureSharpen {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
&DESCRIPTOR
|
DESCRIPTOR.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
//! the band acts at full strength right up to a hard edge; and with two bands
|
//! the band acts at full strength right up to a hard edge; and with two bands
|
||||||
//! adjusted, each one's share depends on what the other is set to, so turning
|
//! adjusted, each one's share depends on what the other is set to, so turning
|
||||||
//! up one colour's saturation quietly weakened its neighbour's hue shift.
|
//! up one colour's saturation quietly weakened its neighbour's hue shift.
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{
|
use crate::descriptor::{
|
||||||
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId,
|
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId,
|
||||||
@@ -123,8 +124,9 @@ impl Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parameter descriptors, one per band per channel. Written out rather than
|
// Parameter descriptors, one per band per channel. Written out rather than
|
||||||
// generated because `ParamDescriptor` must be `const` to live in a `static`,
|
// looped because `concat!` needs literals: every id is built from its band's
|
||||||
// and a const loop cannot build a slice. The macro keeps it honest.
|
// key, and a runtime loop has no way to spell `orange_sat`. The macro keeps it
|
||||||
|
// honest.
|
||||||
//
|
//
|
||||||
// **Every one of them is faceted**, and that is what makes the operation
|
// **Every one of them is faceted**, and that is what makes the operation
|
||||||
// legible in a panel. Thirty-six parameters presented as a flat list are
|
// legible in a panel. Thirty-six parameters presented as a flat list are
|
||||||
@@ -137,7 +139,7 @@ impl Channel {
|
|||||||
// §4.3a); this only says the parameter acts on the band centred there.
|
// §4.3a); this only says the parameter acts on the band centred there.
|
||||||
macro_rules! band_params {
|
macro_rules! band_params {
|
||||||
($(($key:literal, $hue:literal)),* $(,)?) => {
|
($(($key:literal, $hue:literal)),* $(,)?) => {
|
||||||
&[
|
vec![
|
||||||
$(
|
$(
|
||||||
ParamDescriptor::amount(
|
ParamDescriptor::amount(
|
||||||
concat!($key, "_hue"),
|
concat!($key, "_hue"),
|
||||||
@@ -175,25 +177,27 @@ macro_rules! band_params {
|
|||||||
// the same reason as those: `concat!` needs literals, so the keys and hues
|
// the same reason as those: `concat!` needs literals, so the keys and hues
|
||||||
// cannot be read out of `BANDS` here. `facets_match_their_bands` below is
|
// cannot be read out of `BANDS` here. `facets_match_their_bands` below is
|
||||||
// what keeps them from drifting.
|
// what keeps them from drifting.
|
||||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
attributes: &[Attribute::Colour],
|
Arc::new(OpDescriptor {
|
||||||
id: ID,
|
attributes: vec![Attribute::Colour],
|
||||||
label: LocalizedKey("op.colour_mixer"),
|
id: ID,
|
||||||
params: band_params![
|
label: LocalizedKey("op.colour_mixer"),
|
||||||
("red", 0.0),
|
params: band_params![
|
||||||
("orange", 30.0),
|
("red", 0.0),
|
||||||
("yellow", 60.0),
|
("orange", 30.0),
|
||||||
("chartreuse", 90.0),
|
("yellow", 60.0),
|
||||||
("green", 120.0),
|
("chartreuse", 90.0),
|
||||||
("spring", 150.0),
|
("green", 120.0),
|
||||||
("cyan", 180.0),
|
("spring", 150.0),
|
||||||
("azure", 210.0),
|
("cyan", 180.0),
|
||||||
("blue", 240.0),
|
("azure", 210.0),
|
||||||
("violet", 270.0),
|
("blue", 240.0),
|
||||||
("magenta", 300.0),
|
("violet", 270.0),
|
||||||
("rose", 330.0),
|
("magenta", 300.0),
|
||||||
],
|
("rose", 330.0),
|
||||||
};
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
static MIXER_HELPERS: &[Helper] = &[
|
static MIXER_HELPERS: &[Helper] = &[
|
||||||
helpers::LUMINANCE,
|
helpers::LUMINANCE,
|
||||||
@@ -306,8 +310,8 @@ impl ColourMixer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for ColourMixer {
|
impl Operation for ColourMixer {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
&DESCRIPTOR
|
DESCRIPTOR.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||||
@@ -505,7 +509,7 @@ mod tests {
|
|||||||
fn every_descriptor_id_resolves_to_a_band_and_channel() {
|
fn every_descriptor_id_resolves_to_a_band_and_channel() {
|
||||||
// The link between the descriptor list and the value array. A
|
// The link between the descriptor list and the value array. A
|
||||||
// mismatch would make a slider silently adjust nothing.
|
// mismatch would make a slider silently adjust nothing.
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
assert!(
|
assert!(
|
||||||
ColourMixer::index_of(p.id).is_some(),
|
ColourMixer::index_of(p.id).is_some(),
|
||||||
"{} does not map to a band",
|
"{} does not map to a band",
|
||||||
@@ -547,7 +551,7 @@ mod tests {
|
|||||||
// and a hue mistyped there would put a row's swatch on a colour the
|
// and a hue mistyped there would put a row's swatch on a colour the
|
||||||
// band does not act on — a control that lies about what it edits,
|
// band does not act on — a control that lies about what it edits,
|
||||||
// which is worse than one with no swatch at all.
|
// which is worse than one with no swatch at all.
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
let facet = p.facet.expect("every mixer parameter is faceted");
|
let facet = p.facet.expect("every mixer parameter is faceted");
|
||||||
let (band_key, _) = p.id.0.rsplit_once('_').expect("id is band_channel");
|
let (band_key, _) = p.id.0.rsplit_once('_').expect("id is band_channel");
|
||||||
let band = BANDS
|
let band = BANDS
|
||||||
@@ -577,7 +581,7 @@ mod tests {
|
|||||||
// the aspect keyed per band, grouping by it would produce thirty-six
|
// the aspect keyed per band, grouping by it would produce thirty-six
|
||||||
// groups of one and nothing would have been gained.
|
// groups of one and nothing would have been gained.
|
||||||
let mut per_aspect = std::collections::BTreeMap::new();
|
let mut per_aspect = std::collections::BTreeMap::new();
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
let facet = p.facet.expect("faceted");
|
let facet = p.facet.expect("faceted");
|
||||||
*per_aspect.entry(facet.aspect.0).or_insert(0) += 1;
|
*per_aspect.entry(facet.aspect.0).or_insert(0) += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@
|
|||||||
//! whole composition scheme rests on (ARCH §5.6).
|
//! whole composition scheme rests on (ARCH §5.6).
|
||||||
|
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{
|
use crate::descriptor::{
|
||||||
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation,
|
Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation,
|
||||||
@@ -341,11 +342,12 @@ const fn facet_of(aspect: &'static str, channel: Channel) -> Facet {
|
|||||||
|
|
||||||
/// One channel's ten descriptors, defaulted onto the identity diagonal.
|
/// One channel's ten descriptors, defaulted onto the identity diagonal.
|
||||||
///
|
///
|
||||||
/// Written out per point rather than looped because a `ParamDescriptor` has to
|
/// Written out per point rather than looped because `concat!` needs literals:
|
||||||
/// be `const` to live in a `static`, and a const loop cannot build a slice.
|
/// the parameter ids are built from the channel's prefix, and a runtime loop
|
||||||
|
/// has no way to spell `r_p0_x`.
|
||||||
macro_rules! channel_params {
|
macro_rules! channel_params {
|
||||||
($(($prefix:literal, $channel:expr)),* $(,)?) => {
|
($(($prefix:literal, $channel:expr)),* $(,)?) => {
|
||||||
&[$(
|
vec![$(
|
||||||
coord(concat!($prefix, "p0_x"), "param.curve.p0_x", 0.0)
|
coord(concat!($prefix, "p0_x"), "param.curve.p0_x", 0.0)
|
||||||
.faceted(facet_of("param.curve.p0_x", $channel)),
|
.faceted(facet_of("param.curve.p0_x", $channel)),
|
||||||
coord(concat!($prefix, "p0_y"), "param.curve.p0_y", 0.0)
|
coord(concat!($prefix, "p0_y"), "param.curve.p0_y", 0.0)
|
||||||
@@ -370,26 +372,28 @@ macro_rules! channel_params {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
// Both, and this is the case the plural exists for: the master curve is
|
Arc::new(OpDescriptor {
|
||||||
// tonal and the per-channel curves are chromatic. Filing it under one
|
// Both, and this is the case the plural exists for: the master curve is
|
||||||
// would hide it from half the people looking for it.
|
// tonal and the per-channel curves are chromatic. Filing it under one
|
||||||
attributes: &[Attribute::Tone, Attribute::Colour],
|
// would hide it from half the people looking for it.
|
||||||
id: ID,
|
attributes: vec![Attribute::Tone, Attribute::Colour],
|
||||||
label: LocalizedKey("op.tone_curve"),
|
id: ID,
|
||||||
// Defaults lie on y = x, so a fresh curve is the identity and the
|
label: LocalizedKey("op.tone_curve"),
|
||||||
// operation reports itself inactive — on every channel.
|
// Defaults lie on y = x, so a fresh curve is the identity and the
|
||||||
//
|
// operation reports itself inactive — on every channel.
|
||||||
// The master's ten come first, and stay first: a frontend addresses a
|
//
|
||||||
// point by its offset from the first parameter of the run it is drawing,
|
// The master's ten come first, and stay first: a frontend addresses a
|
||||||
// and this is also the order one falling back to sliders reads them in.
|
// point by its offset from the first parameter of the run it is drawing,
|
||||||
params: channel_params![
|
// and this is also the order one falling back to sliders reads them in.
|
||||||
("", Channel::Master),
|
params: channel_params![
|
||||||
("r_", Channel::Red),
|
("", Channel::Master),
|
||||||
("g_", Channel::Green),
|
("r_", Channel::Red),
|
||||||
("b_", Channel::Blue),
|
("g_", Channel::Green),
|
||||||
],
|
("b_", Channel::Blue),
|
||||||
};
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
/// One span of a monotone cubic Hermite spline. Shared by all four curves.
|
/// One span of a monotone cubic Hermite spline. Shared by all four curves.
|
||||||
const CURVE_SPAN: Helper = Helper {
|
const CURVE_SPAN: Helper = Helper {
|
||||||
@@ -699,8 +703,8 @@ impl ToneCurve {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for ToneCurve {
|
impl Operation for ToneCurve {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
&DESCRIPTOR
|
DESCRIPTOR.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||||
@@ -727,7 +731,7 @@ impl Operation for ToneCurve {
|
|||||||
Some(Presentation {
|
Some(Presentation {
|
||||||
// One entry: there is no second way to draw a tone curve that is
|
// One entry: there is no second way to draw a tone curve that is
|
||||||
// better than the sliders the frontend falls back to anyway.
|
// better than the sliders the frontend falls back to anyway.
|
||||||
widgets: &[WidgetKind::ToneCurve],
|
widgets: vec![WidgetKind::ToneCurve],
|
||||||
demand: WidgetDemand {
|
demand: WidgetDemand {
|
||||||
// A point is dragged in x and y together — that is what a
|
// A point is dragged in x and y together — that is what a
|
||||||
// curve *is*, and a frontend that can only move one axis at a
|
// curve *is*, and a frontend that can only move one axis at a
|
||||||
@@ -739,7 +743,7 @@ impl Operation for ToneCurve {
|
|||||||
// leave the other thirty stranded as sliders beneath the plot;
|
// leave the other thirty stranded as sliders beneath the plot;
|
||||||
// which of the four it draws at a time is its own affair, and the
|
// which of the four it draws at a time is its own affair, and the
|
||||||
// facets are what let it decide without naming a channel.
|
// facets are what let it decide without naming a channel.
|
||||||
params: &CURVE_PARAMS,
|
params: CURVE_PARAMS.to_vec(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -910,7 +914,7 @@ mod tests {
|
|||||||
// Opening an unedited image must show the image.
|
// Opening an unedited image must show the image.
|
||||||
let c = ToneCurve::new();
|
let c = ToneCurve::new();
|
||||||
assert!(!c.is_active());
|
assert!(!c.is_active());
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
assert_eq!(c.param(p.id), p.default);
|
assert_eq!(c.param(p.id), p.default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -927,7 +931,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_parameter_id_maps_to_a_point() {
|
fn every_parameter_id_maps_to_a_point() {
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
assert!(
|
assert!(
|
||||||
ToneCurve::index_of(p.id).is_some(),
|
ToneCurve::index_of(p.id).is_some(),
|
||||||
"{} does not map to a point",
|
"{} does not map to a point",
|
||||||
@@ -1196,7 +1200,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(presentation.choose(|_| false), None);
|
assert_eq!(presentation.choose(|_| false), None);
|
||||||
assert_eq!(presentation.params.len(), DESCRIPTOR.params.len());
|
assert_eq!(presentation.params.len(), DESCRIPTOR.params.len());
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
assert!(
|
assert!(
|
||||||
presentation.params.contains(&p.id),
|
presentation.params.contains(&p.id),
|
||||||
"{} is not owned by the widget",
|
"{} is not owned by the widget",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
//! set three correlated coefficients, and hand-correcting a lens with no
|
//! set three correlated coefficients, and hand-correcting a lens with no
|
||||||
//! profile is a "make the horizon straight" task, which one term does well.
|
//! profile is a "make the horizon straight" task, which one term does well.
|
||||||
//! The full triple is reachable by loading a profile.
|
//! The full triple is reachable by loading a profile.
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{
|
use crate::descriptor::{
|
||||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
||||||
@@ -35,25 +36,27 @@ use crate::operation::{Helper, Uniform};
|
|||||||
pub const ID: OpId = OpId("distortion");
|
pub const ID: OpId = OpId("distortion");
|
||||||
pub const AMOUNT: ParamId = ParamId("amount");
|
pub const AMOUNT: ParamId = ParamId("amount");
|
||||||
|
|
||||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
attributes: &[Attribute::Optics],
|
Arc::new(OpDescriptor {
|
||||||
id: ID,
|
attributes: vec![Attribute::Optics],
|
||||||
label: LocalizedKey("op.distortion"),
|
id: ID,
|
||||||
// ±100 maps to a ±0.25 cubic coefficient. That covers an uncorrected
|
label: LocalizedKey("op.distortion"),
|
||||||
// fisheye at one end and strong pincushion at the other; beyond it the
|
// ±100 maps to a ±0.25 cubic coefficient. That covers an uncorrected
|
||||||
// inverse mapping stops being single-valued near the corners and the
|
// fisheye at one end and strong pincushion at the other; beyond it the
|
||||||
// correction folds the image over itself.
|
// inverse mapping stops being single-valued near the corners and the
|
||||||
params: &[ParamDescriptor::scalar(
|
// correction folds the image over itself.
|
||||||
"amount",
|
params: vec![ParamDescriptor::scalar(
|
||||||
"param.distortion.amount",
|
"amount",
|
||||||
-100.0,
|
"param.distortion.amount",
|
||||||
100.0,
|
-100.0,
|
||||||
0.0,
|
100.0,
|
||||||
Unit::None,
|
0.0,
|
||||||
Scale::Linear,
|
Unit::None,
|
||||||
0,
|
Scale::Linear,
|
||||||
)],
|
0,
|
||||||
};
|
)],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
/// The cubic coefficient at full slider travel.
|
/// The cubic coefficient at full slider travel.
|
||||||
const MAX_COEFF: f32 = 0.25;
|
const MAX_COEFF: f32 = 0.25;
|
||||||
@@ -108,8 +111,8 @@ impl Distortion {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Warp for Distortion {
|
impl Warp for Distortion {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
&DESCRIPTOR
|
DESCRIPTOR.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@
|
|||||||
//! Declared as a plain struct here rather than imported, so that dr-pipeline
|
//! Declared as a plain struct here rather than imported, so that dr-pipeline
|
||||||
//! keeps its no-dependency property (ARCH §6.5a) exactly as `vignetting` does
|
//! keeps its no-dependency property (ARCH §6.5a) exactly as `vignetting` does
|
||||||
//! with `Pa`.
|
//! with `Pa`.
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
||||||
use crate::operation::{Operation, Uniform};
|
use crate::operation::{Operation, Uniform};
|
||||||
@@ -73,29 +74,31 @@ static MATRIX_FIELDS: [[&str; 3]; 3] = [
|
|||||||
["m20", "m21", "m22"],
|
["m20", "m21", "m22"],
|
||||||
];
|
];
|
||||||
|
|
||||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
// Tone and colour both, and not `Effect`: a stock is not something applied
|
Arc::new(OpDescriptor {
|
||||||
// on top of a photograph, it is what the photograph was made on.
|
// Tone and colour both, and not `Effect`: a stock is not something applied
|
||||||
attributes: &[Attribute::Tone, Attribute::Colour],
|
// on top of a photograph, it is what the photograph was made on.
|
||||||
id: ID,
|
attributes: vec![Attribute::Tone, Attribute::Colour],
|
||||||
label: LocalizedKey("op.film_sim"),
|
id: ID,
|
||||||
params: &[
|
label: LocalizedKey("op.film_sim"),
|
||||||
ParamDescriptor::stops("exposure", "param.film_sim.exposure", -3.0, 3.0),
|
params: vec![
|
||||||
ParamDescriptor::stops("print_exposure", "param.film_sim.print_exposure", -3.0, 3.0),
|
ParamDescriptor::stops("exposure", "param.film_sim.exposure", -3.0, 3.0),
|
||||||
// TRACES: FR-DEV-3f
|
ParamDescriptor::stops("print_exposure", "param.film_sim.print_exposure", -3.0, 3.0),
|
||||||
// Development, in stops of push. Bounded by what the manufacturers
|
// TRACES: FR-DEV-3f
|
||||||
// actually published: Double-X's measured axis spans about -1 to +2,
|
// Development, in stops of push. Bounded by what the manufacturers
|
||||||
// and beyond a range like that a curve would have to be invented.
|
// actually published: Double-X's measured axis spans about -1 to +2,
|
||||||
ParamDescriptor::stops("push", "param.film_sim.push", -1.0, 3.0),
|
// and beyond a range like that a curve would have to be invented.
|
||||||
// TRACES: FR-DEV-3f
|
ParamDescriptor::stops("push", "param.film_sim.push", -1.0, 3.0),
|
||||||
// Which frame this was taken on — the half of the enlargement a
|
// TRACES: FR-DEV-3f
|
||||||
// photograph cannot supply. A crystal is a fixed size in micrometres,
|
// Which frame this was taken on — the half of the enlargement a
|
||||||
// so how grainy a picture looks is film size against output size, and
|
// photograph cannot supply. A crystal is a fixed size in micrometres,
|
||||||
// the same emulsion on 4x5 renders about three times smoother than on
|
// so how grainy a picture looks is film size against output size, and
|
||||||
// 35mm at the same print.
|
// the same emulsion on 4x5 renders about three times smoother than on
|
||||||
ParamDescriptor::choice("format", "param.film_sim.format", &FORMATS),
|
// 35mm at the same print.
|
||||||
],
|
ParamDescriptor::choice("format", "param.film_sim.format", FORMATS.to_vec()),
|
||||||
};
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
/// A stock reduced to what a shader runs, as `dr-film` bakes it.
|
/// A stock reduced to what a shader runs, as `dr-film` bakes it.
|
||||||
///
|
///
|
||||||
@@ -190,8 +193,8 @@ impl FilmSim {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for FilmSim {
|
impl Operation for FilmSim {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
&DESCRIPTOR
|
DESCRIPTOR.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
//! this file.
|
//! this file.
|
||||||
|
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
||||||
use crate::detail::{DetailPass, DetailStage, RenderScale};
|
use crate::detail::{DetailPass, DetailStage, RenderScale};
|
||||||
@@ -181,7 +182,12 @@ const TRUNCATION: f32 = 2.0;
|
|||||||
/// between clarity and texture can be read side by side, which is the one
|
/// between clarity and texture can be read side by side, which is the one
|
||||||
/// thing a reader comes to this file to do.
|
/// thing a reader comes to this file to do.
|
||||||
pub struct Recipe {
|
pub struct Recipe {
|
||||||
descriptor: &'static OpDescriptor,
|
/// The static this operation's descriptor is built in. A `LazyLock`
|
||||||
|
/// rather than a reference to a descriptor, because a descriptor is an
|
||||||
|
/// owned value handed out as an `Arc` now (FR-PLG-2), and a `const`
|
||||||
|
/// recipe cannot hold an `Arc` — only a reference to the static that
|
||||||
|
/// makes one.
|
||||||
|
descriptor: &'static LazyLock<Arc<OpDescriptor>>,
|
||||||
helpers: &'static [Helper],
|
helpers: &'static [Helper],
|
||||||
/// The Gaussian's σ, as a fraction of the frame's shorter edge.
|
/// The Gaussian's σ, as a fraction of the frame's shorter edge.
|
||||||
sigma: f32,
|
sigma: f32,
|
||||||
@@ -253,19 +259,23 @@ impl Band for Fine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static CLARITY_DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static CLARITY_DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
id: CLARITY,
|
Arc::new(OpDescriptor {
|
||||||
label: LocalizedKey("op.clarity"),
|
id: CLARITY,
|
||||||
params: &[ParamDescriptor::amount("amount", "param.clarity.amount")],
|
label: LocalizedKey("op.clarity"),
|
||||||
attributes: &[Attribute::Detail],
|
params: vec![ParamDescriptor::amount("amount", "param.clarity.amount")],
|
||||||
};
|
attributes: vec![Attribute::Detail],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
static TEXTURE_DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static TEXTURE_DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
id: TEXTURE,
|
Arc::new(OpDescriptor {
|
||||||
label: LocalizedKey("op.texture"),
|
id: TEXTURE,
|
||||||
params: &[ParamDescriptor::amount("amount", "param.texture.amount")],
|
label: LocalizedKey("op.texture"),
|
||||||
attributes: &[Attribute::Detail],
|
params: vec![ParamDescriptor::amount("amount", "param.texture.amount")],
|
||||||
};
|
attributes: vec![Attribute::Detail],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
/// Luminance as a position on a logarithmic scale, floored.
|
/// Luminance as a position on a logarithmic scale, floored.
|
||||||
///
|
///
|
||||||
@@ -394,8 +404,8 @@ impl<B: Band> LocalContrast<B> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<B: Band> Operation for LocalContrast<B> {
|
impl<B: Band> Operation for LocalContrast<B> {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
B::RECIPE.descriptor
|
Arc::clone(B::RECIPE.descriptor)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_param(&mut self, _id: ParamId, value: f32) {
|
fn set_param(&mut self, _id: ParamId, value: f32) {
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ mod tests {
|
|||||||
// perfectly and silently breaks the sidecar.
|
// perfectly and silently breaks the sidecar.
|
||||||
for mut op in chain() {
|
for mut op in chain() {
|
||||||
let descriptor = op.descriptor();
|
let descriptor = op.descriptor();
|
||||||
for p in descriptor.params {
|
for p in &descriptor.params {
|
||||||
let crate::descriptor::ParamKind::Scalar { min, max, .. } = p.kind else {
|
let crate::descriptor::ParamKind::Scalar { min, max, .. } = p.kind else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -147,6 +147,7 @@
|
|||||||
//! lie. The chroma radius, ten times larger, still resolves — which is also
|
//! lie. The chroma radius, ten times larger, still resolves — which is also
|
||||||
//! true of the fault it treats, since a blotch twenty pixels across survives
|
//! true of the fault it treats, since a blotch twenty pixels across survives
|
||||||
//! being halved.
|
//! being halved.
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{
|
use crate::descriptor::{
|
||||||
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit,
|
||||||
@@ -158,38 +159,40 @@ pub const ID: OpId = OpId("noise_reduction");
|
|||||||
pub const LUMINANCE: ParamId = ParamId("luminance");
|
pub const LUMINANCE: ParamId = ParamId("luminance");
|
||||||
pub const CHROMA: ParamId = ParamId("chroma");
|
pub const CHROMA: ParamId = ParamId("chroma");
|
||||||
|
|
||||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
id: ID,
|
Arc::new(OpDescriptor {
|
||||||
label: LocalizedKey("op.noise_reduction"),
|
id: ID,
|
||||||
attributes: &[Attribute::Detail],
|
label: LocalizedKey("op.noise_reduction"),
|
||||||
// Zero to a hundred rather than the symmetric `amount` shape the tonal
|
attributes: vec![Attribute::Detail],
|
||||||
// controls use. There is no meaningful negative: "minus fifty noise
|
// Zero to a hundred rather than the symmetric `amount` shape the tonal
|
||||||
// reduction" would be adding grain, which is a look rather than a repair
|
// controls use. There is no meaningful negative: "minus fifty noise
|
||||||
// and belongs to a different operation carrying `Attribute::Effect`. A
|
// reduction" would be adding grain, which is a look rather than a repair
|
||||||
// control whose left half does nothing is worse than one that stops.
|
// and belongs to a different operation carrying `Attribute::Effect`. A
|
||||||
params: &[
|
// control whose left half does nothing is worse than one that stops.
|
||||||
ParamDescriptor::scalar(
|
params: vec![
|
||||||
"luminance",
|
ParamDescriptor::scalar(
|
||||||
"param.noise_reduction.luminance",
|
"luminance",
|
||||||
0.0,
|
"param.noise_reduction.luminance",
|
||||||
100.0,
|
0.0,
|
||||||
0.0,
|
100.0,
|
||||||
Unit::None,
|
0.0,
|
||||||
Scale::Linear,
|
Unit::None,
|
||||||
0,
|
Scale::Linear,
|
||||||
),
|
0,
|
||||||
ParamDescriptor::scalar(
|
),
|
||||||
"chroma",
|
ParamDescriptor::scalar(
|
||||||
"param.noise_reduction.chroma",
|
"chroma",
|
||||||
0.0,
|
"param.noise_reduction.chroma",
|
||||||
100.0,
|
0.0,
|
||||||
0.0,
|
100.0,
|
||||||
Unit::None,
|
0.0,
|
||||||
Scale::Linear,
|
Unit::None,
|
||||||
0,
|
Scale::Linear,
|
||||||
),
|
0,
|
||||||
],
|
),
|
||||||
};
|
],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
/// The luminance radius at the lowest and the highest amount, in **source**
|
/// The luminance radius at the lowest and the highest amount, in **source**
|
||||||
/// pixels.
|
/// pixels.
|
||||||
@@ -365,8 +368,8 @@ fn inv_spatial(kernel: u32) -> f32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for NoiseReduction {
|
impl Operation for NoiseReduction {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
&DESCRIPTOR
|
DESCRIPTOR.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
//! division is the whole reason this operation must run before the tonal
|
//! division is the whole reason this operation must run before the tonal
|
||||||
//! stages: a corner recovered by two stops has to be recovered while the
|
//! stages: a corner recovered by two stops has to be recovered while the
|
||||||
//! highlight headroom to hold it still exists (ARCH §5.2).
|
//! highlight headroom to hold it still exists (ARCH §5.2).
|
||||||
|
use std::sync::{Arc, LazyLock};
|
||||||
|
|
||||||
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
|
||||||
use crate::operation::{Helper, Operation, Uniform};
|
use crate::operation::{Helper, Operation, Uniform};
|
||||||
@@ -45,19 +46,21 @@ pub const AMOUNT: ParamId = ParamId("amount");
|
|||||||
/// fast prime wide open — the case that actually needs correcting.
|
/// fast prime wide open — the case that actually needs correcting.
|
||||||
const MAX_K1: f32 = -0.5;
|
const MAX_K1: f32 = -0.5;
|
||||||
|
|
||||||
static DESCRIPTOR: OpDescriptor = OpDescriptor {
|
static DESCRIPTOR: LazyLock<Arc<OpDescriptor>> = LazyLock::new(|| {
|
||||||
// Optics rather than effect: this carries lens-profile coefficients
|
Arc::new(OpDescriptor {
|
||||||
// and corrects what the lens did. A *creative* vignette is a different
|
// Optics rather than effect: this carries lens-profile coefficients
|
||||||
// operation that does not exist yet, and would be `Effect`.
|
// and corrects what the lens did. A *creative* vignette is a different
|
||||||
attributes: &[Attribute::Optics],
|
// operation that does not exist yet, and would be `Effect`.
|
||||||
id: ID,
|
attributes: vec![Attribute::Optics],
|
||||||
label: LocalizedKey("op.vignetting"),
|
id: ID,
|
||||||
// Bidirectional deliberately. Negative values *add* falloff, which is a
|
label: LocalizedKey("op.vignetting"),
|
||||||
// legitimate creative choice as well as a correction, and a control that
|
// Bidirectional deliberately. Negative values *add* falloff, which is a
|
||||||
// only removed vignetting would need a second one beside it to put any
|
// legitimate creative choice as well as a correction, and a control that
|
||||||
// back.
|
// only removed vignetting would need a second one beside it to put any
|
||||||
params: &[ParamDescriptor::amount("amount", "param.vignetting.amount")],
|
// back.
|
||||||
};
|
params: vec![ParamDescriptor::amount("amount", "param.vignetting.amount")],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
/// The `pa` polynomial coefficients, as Lensfun stores them.
|
/// The `pa` polynomial coefficients, as Lensfun stores them.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
@@ -107,8 +110,8 @@ impl Vignetting {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Operation for Vignetting {
|
impl Operation for Vignetting {
|
||||||
fn descriptor(&self) -> &'static OpDescriptor {
|
fn descriptor(&self) -> Arc<OpDescriptor> {
|
||||||
&DESCRIPTOR
|
DESCRIPTOR.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_param(&mut self, id: ParamId, value: f32) {
|
fn set_param(&mut self, id: ParamId, value: f32) {
|
||||||
@@ -371,7 +374,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn every_default_is_neutral() {
|
fn every_default_is_neutral() {
|
||||||
let mut v = Vignetting::new();
|
let mut v = Vignetting::new();
|
||||||
for p in DESCRIPTOR.params {
|
for p in &DESCRIPTOR.params {
|
||||||
v.set_param(p.id, p.default);
|
v.set_param(p.id, p.default);
|
||||||
}
|
}
|
||||||
assert!(!v.is_active());
|
assert!(!v.is_active());
|
||||||
|
|||||||
@@ -1297,8 +1297,17 @@ impl PartialMask {
|
|||||||
.ops
|
.ops
|
||||||
.iter()
|
.iter()
|
||||||
.find(|o| o.descriptor().id.0 == op)
|
.find(|o| o.descriptor().id.0 == op)
|
||||||
.and_then(|o| o.descriptor().params.iter().find(|p| p.id.0 == param))
|
// The descriptor is bound inside the closure rather than
|
||||||
.map(|p| p.id)
|
// chained through: it is an owned `Arc` now, so a
|
||||||
|
// `ParamDescriptor` borrowed out of it would not outlive the
|
||||||
|
// expression. The `ParamId` is `Copy`, so it does.
|
||||||
|
.and_then(|o| {
|
||||||
|
o.descriptor()
|
||||||
|
.params
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.id.0 == param)
|
||||||
|
.map(|p| p.id)
|
||||||
|
})
|
||||||
else {
|
else {
|
||||||
log::warn!("sidecar: unknown mask parameter {op}.{param}; ignoring");
|
log::warn!("sidecar: unknown mask parameter {op}.{param}; ignoring");
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+24
-24
@@ -9,17 +9,17 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
|||||||
|
|
||||||
| Metric | Value |
|
| Metric | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Source files scanned | 254 |
|
| Source files scanned | 257 |
|
||||||
| TRACES tags found | 732 |
|
| TRACES tags found | 746 |
|
||||||
| Requirements defined | 177 |
|
| Requirements defined | 177 |
|
||||||
| Requirements covered | 98 |
|
| Requirements covered | 100 |
|
||||||
| **Coverage** | **55.4%** (98/177) |
|
| **Coverage** | **56.5%** (100/177) |
|
||||||
|
|
||||||
### By type
|
### By type
|
||||||
|
|
||||||
| Type | Covered | Defined |
|
| Type | Covered | Defined |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| FR | 78 | 122 |
|
| FR | 80 | 122 |
|
||||||
| NFR | 18 | 49 |
|
| NFR | 18 | 49 |
|
||||||
| R | 2 | 6 |
|
| R | 2 | 6 |
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ _None._
|
|||||||
| FR-CAT-5 | [`core/dr-catalog/src/keywords.rs:1`](../core/dr-catalog/src/keywords.rs#L1), [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-catalog/src/schema.rs:1001`](../core/dr-catalog/src/schema.rs#L1001), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`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:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`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:3110`](../ui/dr-ui/src/library.rs#L3110), [`ui/dr-ui/src/library_ui.rs:6271`](../ui/dr-ui/src/library_ui.rs#L6271), [`ui/dr-ui/src/library_ui.rs:6282`](../ui/dr-ui/src/library_ui.rs#L6282), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6295`](../ui/dr-ui/src/library_ui.rs#L6295), [`ui/dr-ui/src/library_ui.rs:6310`](../ui/dr-ui/src/library_ui.rs#L6310), [`ui/dr-ui/src/library_ui.rs:6319`](../ui/dr-ui/src/library_ui.rs#L6319), [`ui/dr-ui/ui/app.slint:485`](../ui/dr-ui/ui/app.slint#L485), [`ui/dr-ui/ui/app.slint:655`](../ui/dr-ui/ui/app.slint#L655), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1222`](../ui/dr-ui/ui/library.slint#L1222), [`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:902`](../ui/dr-ui/ui/library.slint#L902) |
|
| 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:1001`](../core/dr-catalog/src/schema.rs#L1001), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`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:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`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:3110`](../ui/dr-ui/src/library.rs#L3110), [`ui/dr-ui/src/library_ui.rs:6271`](../ui/dr-ui/src/library_ui.rs#L6271), [`ui/dr-ui/src/library_ui.rs:6282`](../ui/dr-ui/src/library_ui.rs#L6282), [`ui/dr-ui/src/library_ui.rs:628`](../ui/dr-ui/src/library_ui.rs#L628), [`ui/dr-ui/src/library_ui.rs:6295`](../ui/dr-ui/src/library_ui.rs#L6295), [`ui/dr-ui/src/library_ui.rs:6310`](../ui/dr-ui/src/library_ui.rs#L6310), [`ui/dr-ui/src/library_ui.rs:6319`](../ui/dr-ui/src/library_ui.rs#L6319), [`ui/dr-ui/ui/app.slint:485`](../ui/dr-ui/ui/app.slint#L485), [`ui/dr-ui/ui/app.slint:655`](../ui/dr-ui/ui/app.slint#L655), [`ui/dr-ui/ui/library.slint:1219`](../ui/dr-ui/ui/library.slint#L1219), [`ui/dr-ui/ui/library.slint:1222`](../ui/dr-ui/ui/library.slint#L1222), [`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:902`](../ui/dr-ui/ui/library.slint#L902) |
|
||||||
| 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:498`](../core/dr-catalog/src/schema.rs#L498), [`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:3356`](../ui/dr-ui/src/library.rs#L3356), [`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:5140`](../ui/dr-ui/src/library_ui.rs#L5140), [`ui/dr-ui/src/library_ui.rs:5193`](../ui/dr-ui/src/library_ui.rs#L5193), [`ui/dr-ui/src/library_ui.rs:6411`](../ui/dr-ui/src/library_ui.rs#L6411), [`ui/dr-ui/ui/app.slint:488`](../ui/dr-ui/ui/app.slint#L488), [`ui/dr-ui/ui/app.slint:655`](../ui/dr-ui/ui/app.slint#L655), [`ui/dr-ui/ui/app.slint:894`](../ui/dr-ui/ui/app.slint#L894), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1288`](../ui/dr-ui/ui/library.slint#L1288), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902), [`ui/dr-ui/ui/settings.slint:128`](../ui/dr-ui/ui/settings.slint#L128) |
|
| 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:498`](../core/dr-catalog/src/schema.rs#L498), [`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:3356`](../ui/dr-ui/src/library.rs#L3356), [`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:5140`](../ui/dr-ui/src/library_ui.rs#L5140), [`ui/dr-ui/src/library_ui.rs:5193`](../ui/dr-ui/src/library_ui.rs#L5193), [`ui/dr-ui/src/library_ui.rs:6411`](../ui/dr-ui/src/library_ui.rs#L6411), [`ui/dr-ui/ui/app.slint:488`](../ui/dr-ui/ui/app.slint#L488), [`ui/dr-ui/ui/app.slint:655`](../ui/dr-ui/ui/app.slint#L655), [`ui/dr-ui/ui/app.slint:894`](../ui/dr-ui/ui/app.slint#L894), [`ui/dr-ui/ui/library.slint:109`](../ui/dr-ui/ui/library.slint#L109), [`ui/dr-ui/ui/library.slint:1288`](../ui/dr-ui/ui/library.slint#L1288), [`ui/dr-ui/ui/library.slint:902`](../ui/dr-ui/ui/library.slint#L902), [`ui/dr-ui/ui/settings.slint:128`](../ui/dr-ui/ui/settings.slint#L128) |
|
||||||
| 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:3482`](../ui/dr-ui/src/library_ui.rs#L3482), [`ui/dr-ui/src/library_ui.rs:4180`](../ui/dr-ui/src/library_ui.rs#L4180), [`ui/dr-ui/ui/app.slint:650`](../ui/dr-ui/ui/app.slint#L650), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2528`](../ui/dr-ui/ui/library.slint#L2528), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) |
|
| 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:3482`](../ui/dr-ui/src/library_ui.rs#L3482), [`ui/dr-ui/src/library_ui.rs:4180`](../ui/dr-ui/src/library_ui.rs#L4180), [`ui/dr-ui/ui/app.slint:650`](../ui/dr-ui/ui/app.slint#L650), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4), [`ui/dr-ui/ui/library.slint:2528`](../ui/dr-ui/ui/library.slint#L2528), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) |
|
||||||
| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`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:1627`](../core/dr-pipeline/src/sidecar.rs#L1627), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:3316`](../ui/dr-ui/src/develop.rs#L3316), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1365`](../ui/dr-ui/src/lib.rs#L1365), [`ui/dr-ui/src/lib.rs:1738`](../ui/dr-ui/src/lib.rs#L1738), [`ui/dr-ui/src/lib.rs:1874`](../ui/dr-ui/src/lib.rs#L1874), [`ui/dr-ui/src/lib.rs:480`](../ui/dr-ui/src/lib.rs#L480), [`ui/dr-ui/src/lib.rs:905`](../ui/dr-ui/src/lib.rs#L905), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`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:4882`](../ui/dr-ui/src/library_ui.rs#L4882), [`ui/dr-ui/src/sidecar_cache.rs:1`](../ui/dr-ui/src/sidecar_cache.rs#L1) |
|
| FR-CAT-8 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/ops/curve.rs:137`](../core/dr-pipeline/src/ops/curve.rs#L137), [`core/dr-pipeline/src/ops/curve.rs:656`](../core/dr-pipeline/src/ops/curve.rs#L656), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`core/dr-pipeline/tests/tone_curve.rs:34`](../core/dr-pipeline/tests/tone_curve.rs#L34), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:3316`](../ui/dr-ui/src/develop.rs#L3316), [`ui/dr-ui/src/export.rs:752`](../ui/dr-ui/src/export.rs#L752), [`ui/dr-ui/src/lib.rs:1365`](../ui/dr-ui/src/lib.rs#L1365), [`ui/dr-ui/src/lib.rs:1738`](../ui/dr-ui/src/lib.rs#L1738), [`ui/dr-ui/src/lib.rs:1874`](../ui/dr-ui/src/lib.rs#L1874), [`ui/dr-ui/src/lib.rs:480`](../ui/dr-ui/src/lib.rs#L480), [`ui/dr-ui/src/lib.rs:905`](../ui/dr-ui/src/lib.rs#L905), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`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:4882`](../ui/dr-ui/src/library_ui.rs#L4882), [`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:555`](../core/dr-catalog/src/schema.rs#L555), [`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:2812`](../ui/dr-ui/src/develop.rs#L2812), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1511`](../ui/dr-ui/src/library.rs#L1511), [`ui/dr-ui/src/library.rs:1588`](../ui/dr-ui/src/library.rs#L1588), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3463`](../ui/dr-ui/src/library.rs#L3463), [`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:3345`](../ui/dr-ui/src/library_ui.rs#L3345), [`ui/dr-ui/src/library_ui.rs:3533`](../ui/dr-ui/src/library_ui.rs#L3533), [`ui/dr-ui/src/library_ui.rs:3651`](../ui/dr-ui/src/library_ui.rs#L3651), [`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:5238`](../ui/dr-ui/src/library_ui.rs#L5238), [`ui/dr-ui/src/library_ui.rs:5353`](../ui/dr-ui/src/library_ui.rs#L5353), [`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-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:555`](../core/dr-catalog/src/schema.rs#L555), [`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:2812`](../ui/dr-ui/src/develop.rs#L2812), [`ui/dr-ui/src/library.rs:138`](../ui/dr-ui/src/library.rs#L138), [`ui/dr-ui/src/library.rs:1511`](../ui/dr-ui/src/library.rs#L1511), [`ui/dr-ui/src/library.rs:1588`](../ui/dr-ui/src/library.rs#L1588), [`ui/dr-ui/src/library.rs:220`](../ui/dr-ui/src/library.rs#L220), [`ui/dr-ui/src/library.rs:3463`](../ui/dr-ui/src/library.rs#L3463), [`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:3345`](../ui/dr-ui/src/library_ui.rs#L3345), [`ui/dr-ui/src/library_ui.rs:3533`](../ui/dr-ui/src/library_ui.rs#L3533), [`ui/dr-ui/src/library_ui.rs:3651`](../ui/dr-ui/src/library_ui.rs#L3651), [`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:5238`](../ui/dr-ui/src/library_ui.rs#L5238), [`ui/dr-ui/src/library_ui.rs:5353`](../ui/dr-ui/src/library_ui.rs#L5353), [`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:121`](../core/dr-decode/src/preview.rs#L121) |
|
| FR-CULL-1 | [`core/dr-decode/src/preview.rs:121`](../core/dr-decode/src/preview.rs#L121) |
|
||||||
| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/develop.rs:119`](../ui/dr-ui/src/develop.rs#L119), [`ui/dr-ui/src/develop.rs:128`](../ui/dr-ui/src/develop.rs#L128), [`ui/dr-ui/src/develop.rs:1776`](../ui/dr-ui/src/develop.rs#L1776), [`ui/dr-ui/src/develop.rs:194`](../ui/dr-ui/src/develop.rs#L194), [`ui/dr-ui/src/develop.rs:589`](../ui/dr-ui/src/develop.rs#L589), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1846`](../ui/dr-ui/src/lib.rs#L1846), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
| FR-CULL-10 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/develop.rs:119`](../ui/dr-ui/src/develop.rs#L119), [`ui/dr-ui/src/develop.rs:128`](../ui/dr-ui/src/develop.rs#L128), [`ui/dr-ui/src/develop.rs:1776`](../ui/dr-ui/src/develop.rs#L1776), [`ui/dr-ui/src/develop.rs:194`](../ui/dr-ui/src/develop.rs#L194), [`ui/dr-ui/src/develop.rs:589`](../ui/dr-ui/src/develop.rs#L589), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity.rs:1`](../ui/dr-ui/src/identity.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1), [`ui/dr-ui/src/lib.rs:1846`](../ui/dr-ui/src/lib.rs#L1846), [`ui/dr-ui/ui/identity.slint:1`](../ui/dr-ui/ui/identity.slint#L1) |
|
||||||
@@ -56,25 +56,25 @@ _None._
|
|||||||
| FR-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`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-CULL-4 | [`core/dr-catalog/src/rating.rs:1`](../core/dr-catalog/src/rating.rs#L1), [`core/dr-pipeline/src/sidecar.rs:135`](../core/dr-pipeline/src/sidecar.rs#L135), [`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-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:343`](../core/dr-catalog/src/schema.rs#L343), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/ui/settings.slint:385`](../ui/dr-ui/ui/settings.slint#L385), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) |
|
| FR-CULL-8 | [`core/dr-catalog/src/face_shard.rs:1`](../core/dr-catalog/src/face_shard.rs#L1), [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:343`](../core/dr-catalog/src/schema.rs#L343), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/ui/settings.slint:385`](../ui/dr-ui/ui/settings.slint#L385), [`ui/dr-ui/ui/settings.slint:81`](../ui/dr-ui/ui/settings.slint#L81) |
|
||||||
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) |
|
| FR-CULL-9 | [`core/dr-catalog/src/faces.rs:1`](../core/dr-catalog/src/faces.rs#L1), [`core/dr-catalog/src/schema.rs:384`](../core/dr-catalog/src/schema.rs#L384), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1), [`ui/dr-ui/src/identity_ui.rs:1`](../ui/dr-ui/src/identity_ui.rs#L1) |
|
||||||
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:352`](../core/dr-pipeline/src/operation.rs#L352) |
|
| FR-DEV-2 | [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389) |
|
||||||
| 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:362`](../core/dr-pipeline/src/framing.rs#L362), [`core/dr-pipeline/src/framing.rs:617`](../core/dr-pipeline/src/framing.rs#L617), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:573`](../core/dr-pipeline/src/graph.rs#L573), [`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:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1627`](../core/dr-pipeline/src/sidecar.rs#L1627), [`core/dr-pipeline/src/sidecar.rs:1687`](../core/dr-pipeline/src/sidecar.rs#L1687), [`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:1280`](../ui/dr-ui/src/develop.rs#L1280), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1758`](../ui/dr-ui/src/develop.rs#L1758), [`ui/dr-ui/src/develop.rs:1776`](../ui/dr-ui/src/develop.rs#L1776), [`ui/dr-ui/src/develop.rs:1790`](../ui/dr-ui/src/develop.rs#L1790), [`ui/dr-ui/src/develop.rs:1812`](../ui/dr-ui/src/develop.rs#L1812), [`ui/dr-ui/src/develop.rs:1958`](../ui/dr-ui/src/develop.rs#L1958), [`ui/dr-ui/src/develop.rs:2056`](../ui/dr-ui/src/develop.rs#L2056), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3851`](../ui/dr-ui/src/develop.rs#L3851), [`ui/dr-ui/src/develop.rs:3905`](../ui/dr-ui/src/develop.rs#L3905), [`ui/dr-ui/src/develop.rs:3949`](../ui/dr-ui/src/develop.rs#L3949), [`ui/dr-ui/src/develop.rs:3999`](../ui/dr-ui/src/develop.rs#L3999), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1422`](../ui/dr-ui/src/lib.rs#L1422), [`ui/dr-ui/src/lib.rs:2124`](../ui/dr-ui/src/lib.rs#L2124), [`ui/dr-ui/src/lib.rs:301`](../ui/dr-ui/src/lib.rs#L301), [`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:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:2100`](../ui/dr-ui/ui/app.slint#L2100), [`ui/dr-ui/ui/app.slint:981`](../ui/dr-ui/ui/app.slint#L981) |
|
| 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:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:620`](../core/dr-pipeline/src/framing.rs#L620), [`core/dr-pipeline/src/graph.rs:169`](../core/dr-pipeline/src/graph.rs#L169), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`core/dr-pipeline/src/mask.rs:121`](../core/dr-pipeline/src/mask.rs#L121), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:516`](../core/dr-pipeline/src/operation.rs#L516), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:210`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L210), [`core/dr-pipeline/src/ops/curve.rs:100`](../core/dr-pipeline/src/ops/curve.rs#L100), [`core/dr-pipeline/src/ops/curve.rs:1`](../core/dr-pipeline/src/ops/curve.rs#L1), [`core/dr-pipeline/src/ops/curve.rs:219`](../core/dr-pipeline/src/ops/curve.rs#L219), [`core/dr-pipeline/src/ops/curve.rs:635`](../core/dr-pipeline/src/ops/curve.rs#L635), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:1`](../core/dr-pipeline/src/ops/noise_reduction.rs#L1), [`core/dr-pipeline/src/ops/noise_reduction.rs:273`](../core/dr-pipeline/src/ops/noise_reduction.rs#L273), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:1636`](../core/dr-pipeline/src/sidecar.rs#L1636), [`core/dr-pipeline/src/sidecar.rs:1696`](../core/dr-pipeline/src/sidecar.rs#L1696), [`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:1280`](../ui/dr-ui/src/develop.rs#L1280), [`ui/dr-ui/src/develop.rs:163`](../ui/dr-ui/src/develop.rs#L163), [`ui/dr-ui/src/develop.rs:1758`](../ui/dr-ui/src/develop.rs#L1758), [`ui/dr-ui/src/develop.rs:1776`](../ui/dr-ui/src/develop.rs#L1776), [`ui/dr-ui/src/develop.rs:1790`](../ui/dr-ui/src/develop.rs#L1790), [`ui/dr-ui/src/develop.rs:1812`](../ui/dr-ui/src/develop.rs#L1812), [`ui/dr-ui/src/develop.rs:1958`](../ui/dr-ui/src/develop.rs#L1958), [`ui/dr-ui/src/develop.rs:2056`](../ui/dr-ui/src/develop.rs#L2056), [`ui/dr-ui/src/develop.rs:326`](../ui/dr-ui/src/develop.rs#L326), [`ui/dr-ui/src/develop.rs:3287`](../ui/dr-ui/src/develop.rs#L3287), [`ui/dr-ui/src/develop.rs:363`](../ui/dr-ui/src/develop.rs#L363), [`ui/dr-ui/src/develop.rs:3851`](../ui/dr-ui/src/develop.rs#L3851), [`ui/dr-ui/src/develop.rs:3905`](../ui/dr-ui/src/develop.rs#L3905), [`ui/dr-ui/src/develop.rs:3949`](../ui/dr-ui/src/develop.rs#L3949), [`ui/dr-ui/src/develop.rs:3999`](../ui/dr-ui/src/develop.rs#L3999), [`ui/dr-ui/src/develop.rs:628`](../ui/dr-ui/src/develop.rs#L628), [`ui/dr-ui/src/develop.rs:675`](../ui/dr-ui/src/develop.rs#L675), [`ui/dr-ui/src/lib.rs:1422`](../ui/dr-ui/src/lib.rs#L1422), [`ui/dr-ui/src/lib.rs:2124`](../ui/dr-ui/src/lib.rs#L2124), [`ui/dr-ui/src/lib.rs:301`](../ui/dr-ui/src/lib.rs#L301), [`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:219`](../ui/dr-ui/src/segmentation.rs#L219), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322), [`ui/dr-ui/src/segmentation.rs:350`](../ui/dr-ui/src/segmentation.rs#L350), [`ui/dr-ui/ui/app.slint:2100`](../ui/dr-ui/ui/app.slint#L2100), [`ui/dr-ui/ui/app.slint:981`](../ui/dr-ui/ui/app.slint#L981) |
|
||||||
| 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:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`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:1177`](../ui/dr-ui/src/develop.rs#L1177), [`ui/dr-ui/src/lib.rs:595`](../ui/dr-ui/src/lib.rs#L595) |
|
| FR-DEV-3a | [`core/dr-pipeline/build.rs:1814`](../core/dr-pipeline/build.rs#L1814), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/descriptor.rs:194`](../core/dr-pipeline/src/descriptor.rs#L194), [`core/dr-pipeline/src/descriptor.rs:234`](../core/dr-pipeline/src/descriptor.rs#L234), [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/descriptor.rs:313`](../core/dr-pipeline/src/descriptor.rs#L313), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262), [`core/dr-pipeline/src/graph.rs:23`](../core/dr-pipeline/src/graph.rs#L23), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365), [`core/dr-pipeline/src/ops/curve.rs:319`](../core/dr-pipeline/src/ops/curve.rs#L319), [`ui/dr-ui/src/develop.rs:1177`](../ui/dr-ui/src/develop.rs#L1177), [`ui/dr-ui/src/lib.rs:595`](../ui/dr-ui/src/lib.rs#L595) |
|
||||||
| 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:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/operation.rs:328`](../core/dr-pipeline/src/operation.rs#L328) |
|
| FR-DEV-3b | [`core/dr-pipeline/src/descriptor.rs:258`](../core/dr-pipeline/src/descriptor.rs#L258), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262), [`core/dr-pipeline/src/graph.rs:58`](../core/dr-pipeline/src/graph.rs#L58), [`core/dr-pipeline/src/operation.rs:365`](../core/dr-pipeline/src/operation.rs#L365) |
|
||||||
| 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:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/mask.rs:954`](../core/dr-pipeline/src/mask.rs#L954), [`ui/dr-ui/src/develop.rs:4578`](../ui/dr-ui/src/develop.rs#L4578) |
|
| FR-DEV-3c | [`core/dr-pipeline/build.rs:1814`](../core/dr-pipeline/build.rs#L1814), [`core/dr-pipeline/ops/exposure.yaml:1`](../core/dr-pipeline/ops/exposure.yaml#L1), [`core/dr-pipeline/src/graph.rs:250`](../core/dr-pipeline/src/graph.rs#L250), [`core/dr-pipeline/src/graph.rs:45`](../core/dr-pipeline/src/graph.rs#L45), [`core/dr-pipeline/src/mask.rs:955`](../core/dr-pipeline/src/mask.rs#L955), [`ui/dr-ui/src/develop.rs:4578`](../ui/dr-ui/src/develop.rs#L4578) |
|
||||||
| 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:612`](../core/dr-pipeline/src/graph.rs#L612), [`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-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:191`](../core/dr-pipeline/src/framing.rs#L191), [`core/dr-pipeline/src/graph.rs:616`](../core/dr-pipeline/src/graph.rs#L616), [`core/dr-pipeline/src/operation.rs:32`](../core/dr-pipeline/src/operation.rs#L32), [`core/dr-pipeline/src/operation.rs:389`](../core/dr-pipeline/src/operation.rs#L389), [`core/dr-pipeline/src/operation.rs:53`](../core/dr-pipeline/src/operation.rs#L53), [`core/dr-pipeline/src/operation.rs:71`](../core/dr-pipeline/src/operation.rs#L71) |
|
||||||
| FR-DEV-3e | [`core/dr-decode/src/base_curve.rs:145`](../core/dr-decode/src/base_curve.rs#L145), [`core/dr-decode/src/base_curve.rs:158`](../core/dr-decode/src/base_curve.rs#L158), [`core/dr-decode/src/base_curve.rs:1`](../core/dr-decode/src/base_curve.rs#L1), [`core/dr-decode/src/base_curve.rs:267`](../core/dr-decode/src/base_curve.rs#L267), [`core/dr-decode/src/base_curve.rs:347`](../core/dr-decode/src/base_curve.rs#L347), [`core/dr-decode/src/base_curve.rs:55`](../core/dr-decode/src/base_curve.rs#L55), [`core/dr-decode/src/lib.rs:121`](../core/dr-decode/src/lib.rs#L121), [`core/dr-decode/src/lib.rs:708`](../core/dr-decode/src/lib.rs#L708), [`core/dr-decode/src/lib.rs:748`](../core/dr-decode/src/lib.rs#L748), [`core/dr-decode/src/profile.rs:102`](../core/dr-decode/src/profile.rs#L102), [`core/dr-decode/src/profile.rs:151`](../core/dr-decode/src/profile.rs#L151), [`core/dr-decode/src/profile.rs:1`](../core/dr-decode/src/profile.rs#L1), [`core/dr-decode/src/profile.rs:235`](../core/dr-decode/src/profile.rs#L235), [`core/dr-decode/src/profile.rs:286`](../core/dr-decode/src/profile.rs#L286), [`core/dr-decode/src/profile.rs:343`](../core/dr-decode/src/profile.rs#L343), [`core/dr-decode/src/profile.rs:458`](../core/dr-decode/src/profile.rs#L458), [`core/dr-decode/src/profile.rs:492`](../core/dr-decode/src/profile.rs#L492), [`core/dr-decode/src/profile.rs:630`](../core/dr-decode/src/profile.rs#L630), [`core/dr-gpu/src/adjust.rs:37`](../core/dr-gpu/src/adjust.rs#L37), [`core/dr-gpu/src/adjust.rs:967`](../core/dr-gpu/src/adjust.rs#L967), [`core/dr-gpu/src/demosaic.rs:121`](../core/dr-gpu/src/demosaic.rs#L121), [`core/dr-gpu/src/demosaic.rs:86`](../core/dr-gpu/src/demosaic.rs#L86), [`core/dr-gpu/tests/base_curve.rs:1`](../core/dr-gpu/tests/base_curve.rs#L1), [`core/dr-pipeline/src/operation.rs: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-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:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:1576`](../core/dr-pipeline/src/operation.rs#L1576), [`core/dr-pipeline/src/operation.rs:1601`](../core/dr-pipeline/src/operation.rs#L1601), [`core/dr-pipeline/src/operation.rs:1616`](../core/dr-pipeline/src/operation.rs#L1616), [`core/dr-pipeline/src/operation.rs:1640`](../core/dr-pipeline/src/operation.rs#L1640), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/operation.rs:440`](../core/dr-pipeline/src/operation.rs#L440), [`core/dr-pipeline/src/operation.rs:450`](../core/dr-pipeline/src/operation.rs#L450), [`core/dr-pipeline/src/operation.rs:600`](../core/dr-pipeline/src/operation.rs#L600) |
|
||||||
| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/boolean_grain.rs:1`](../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../core/dr-film/src/lib.rs#L160), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:101`](../core/dr-pipeline/src/graph.rs#L101), [`core/dr-pipeline/src/graph.rs:124`](../core/dr-pipeline/src/graph.rs#L124), [`core/dr-pipeline/src/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`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:126`](../core/dr-pipeline/src/ops/film_sim.rs#L126), [`core/dr-pipeline/src/ops/film_sim.rs:150`](../core/dr-pipeline/src/ops/film_sim.rs#L150), [`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:328`](../core/dr-pipeline/src/ops/film_sim.rs#L328), [`core/dr-pipeline/src/ops/film_sim.rs:42`](../core/dr-pipeline/src/ops/film_sim.rs#L42), [`core/dr-pipeline/src/ops/film_sim.rs:85`](../core/dr-pipeline/src/ops/film_sim.rs#L85), [`core/dr-pipeline/src/ops/film_sim.rs:90`](../core/dr-pipeline/src/ops/film_sim.rs#L90), [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:167`](../core/dr-pipeline/src/sidecar.rs#L167), [`core/dr-pipeline/src/sidecar.rs:1948`](../core/dr-pipeline/src/sidecar.rs#L1948), [`core/dr-pipeline/src/sidecar.rs:2024`](../core/dr-pipeline/src/sidecar.rs#L2024), [`core/dr-pipeline/src/sidecar.rs:533`](../core/dr-pipeline/src/sidecar.rs#L533), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:792`](../core/dr-pipeline/src/sidecar.rs#L792), [`core/dr-pipeline/src/state.rs:100`](../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:2844`](../ui/dr-ui/src/develop.rs#L2844), [`ui/dr-ui/src/develop.rs:2856`](../ui/dr-ui/src/develop.rs#L2856), [`ui/dr-ui/src/develop.rs:2894`](../ui/dr-ui/src/develop.rs#L2894), [`ui/dr-ui/src/develop.rs:2903`](../ui/dr-ui/src/develop.rs#L2903), [`ui/dr-ui/src/develop.rs:3006`](../ui/dr-ui/src/develop.rs#L3006), [`ui/dr-ui/src/develop.rs:3324`](../ui/dr-ui/src/develop.rs#L3324), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/lib.rs:2098`](../ui/dr-ui/src/lib.rs#L2098), [`ui/dr-ui/src/lib.rs:528`](../ui/dr-ui/src/lib.rs#L528), [`ui/dr-ui/src/lib.rs:586`](../ui/dr-ui/src/lib.rs#L586), [`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:2712`](../ui/dr-ui/ui/app.slint#L2712), [`ui/dr-ui/ui/app.slint:775`](../ui/dr-ui/ui/app.slint#L775) |
|
| FR-DEV-3f | [`core/dr-film/src/bake.rs:271`](../core/dr-film/src/bake.rs#L271), [`core/dr-film/src/bake.rs:62`](../core/dr-film/src/bake.rs#L62), [`core/dr-film/src/boolean_grain.rs:1`](../core/dr-film/src/boolean_grain.rs#L1), [`core/dr-film/src/boolean_grain.rs:78`](../core/dr-film/src/boolean_grain.rs#L78), [`core/dr-film/src/grain.rs:140`](../core/dr-film/src/grain.rs#L140), [`core/dr-film/src/grain.rs:1`](../core/dr-film/src/grain.rs#L1), [`core/dr-film/src/grain.rs:302`](../core/dr-film/src/grain.rs#L302), [`core/dr-film/src/grain.rs:79`](../core/dr-film/src/grain.rs#L79), [`core/dr-film/src/lib.rs:160`](../core/dr-film/src/lib.rs#L160), [`core/dr-film/src/lib.rs:1`](../core/dr-film/src/lib.rs#L1), [`core/dr-film/src/profile.rs:100`](../core/dr-film/src/profile.rs#L100), [`core/dr-film/src/profile.rs:142`](../core/dr-film/src/profile.rs#L142), [`core/dr-film/src/profile.rs:182`](../core/dr-film/src/profile.rs#L182), [`core/dr-film/src/profile.rs:259`](../core/dr-film/src/profile.rs#L259), [`core/dr-film/src/profile.rs:502`](../core/dr-film/src/profile.rs#L502), [`core/dr-film/src/profile.rs:73`](../core/dr-film/src/profile.rs#L73), [`core/dr-gpu/src/adjust.rs:139`](../core/dr-gpu/src/adjust.rs#L139), [`core/dr-gpu/src/adjust.rs:196`](../core/dr-gpu/src/adjust.rs#L196), [`core/dr-gpu/src/adjust.rs:357`](../core/dr-gpu/src/adjust.rs#L357), [`core/dr-gpu/src/adjust.rs:483`](../core/dr-gpu/src/adjust.rs#L483), [`core/dr-gpu/src/adjust.rs:77`](../core/dr-gpu/src/adjust.rs#L77), [`core/dr-gpu/tests/film_sim.rs:191`](../core/dr-gpu/tests/film_sim.rs#L191), [`core/dr-gpu/tests/film_sim.rs:1`](../core/dr-gpu/tests/film_sim.rs#L1), [`core/dr-pipeline/src/graph.rs:101`](../core/dr-pipeline/src/graph.rs#L101), [`core/dr-pipeline/src/graph.rs:124`](../core/dr-pipeline/src/graph.rs#L124), [`core/dr-pipeline/src/graph.rs:324`](../core/dr-pipeline/src/graph.rs#L324), [`core/dr-pipeline/src/operation.rs:1065`](../core/dr-pipeline/src/operation.rs#L1065), [`core/dr-pipeline/src/operation.rs:1094`](../core/dr-pipeline/src/operation.rs#L1094), [`core/dr-pipeline/src/operation.rs:1495`](../core/dr-pipeline/src/operation.rs#L1495), [`core/dr-pipeline/src/operation.rs:296`](../core/dr-pipeline/src/operation.rs#L296), [`core/dr-pipeline/src/operation.rs:310`](../core/dr-pipeline/src/operation.rs#L310), [`core/dr-pipeline/src/ops/film_sim.rs:129`](../core/dr-pipeline/src/ops/film_sim.rs#L129), [`core/dr-pipeline/src/ops/film_sim.rs:153`](../core/dr-pipeline/src/ops/film_sim.rs#L153), [`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:331`](../core/dr-pipeline/src/ops/film_sim.rs#L331), [`core/dr-pipeline/src/ops/film_sim.rs:43`](../core/dr-pipeline/src/ops/film_sim.rs#L43), [`core/dr-pipeline/src/ops/film_sim.rs:87`](../core/dr-pipeline/src/ops/film_sim.rs#L87), [`core/dr-pipeline/src/ops/film_sim.rs:92`](../core/dr-pipeline/src/ops/film_sim.rs#L92), [`core/dr-pipeline/src/sidecar.rs:111`](../core/dr-pipeline/src/sidecar.rs#L111), [`core/dr-pipeline/src/sidecar.rs:167`](../core/dr-pipeline/src/sidecar.rs#L167), [`core/dr-pipeline/src/sidecar.rs:1957`](../core/dr-pipeline/src/sidecar.rs#L1957), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:533`](../core/dr-pipeline/src/sidecar.rs#L533), [`core/dr-pipeline/src/sidecar.rs:660`](../core/dr-pipeline/src/sidecar.rs#L660), [`core/dr-pipeline/src/sidecar.rs:792`](../core/dr-pipeline/src/sidecar.rs#L792), [`core/dr-pipeline/src/state.rs:100`](../core/dr-pipeline/src/state.rs#L100), [`core/dr-pipeline/src/state.rs:115`](../core/dr-pipeline/src/state.rs#L115), [`core/dr-pipeline/src/state.rs:60`](../core/dr-pipeline/src/state.rs#L60), [`ui/dr-ui/src/develop.rs:2827`](../ui/dr-ui/src/develop.rs#L2827), [`ui/dr-ui/src/develop.rs:2844`](../ui/dr-ui/src/develop.rs#L2844), [`ui/dr-ui/src/develop.rs:2856`](../ui/dr-ui/src/develop.rs#L2856), [`ui/dr-ui/src/develop.rs:2894`](../ui/dr-ui/src/develop.rs#L2894), [`ui/dr-ui/src/develop.rs:2903`](../ui/dr-ui/src/develop.rs#L2903), [`ui/dr-ui/src/develop.rs:3006`](../ui/dr-ui/src/develop.rs#L3006), [`ui/dr-ui/src/develop.rs:3324`](../ui/dr-ui/src/develop.rs#L3324), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/lib.rs:2098`](../ui/dr-ui/src/lib.rs#L2098), [`ui/dr-ui/src/lib.rs:528`](../ui/dr-ui/src/lib.rs#L528), [`ui/dr-ui/src/lib.rs:586`](../ui/dr-ui/src/lib.rs#L586), [`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:2712`](../ui/dr-ui/ui/app.slint#L2712), [`ui/dr-ui/ui/app.slint:775`](../ui/dr-ui/ui/app.slint#L775) |
|
||||||
| 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-pipeline/src/framing.rs:362`](../core/dr-pipeline/src/framing.rs#L362), [`core/dr-pipeline/src/framing.rs:924`](../core/dr-pipeline/src/framing.rs#L924), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/lib.rs:456`](../core/dr-types/src/lib.rs#L456), [`core/dr-types/src/lib.rs:472`](../core/dr-types/src/lib.rs#L472), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) |
|
| 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:205`](../core/dr-pipeline/src/framing.rs#L205), [`core/dr-pipeline/src/framing.rs:365`](../core/dr-pipeline/src/framing.rs#L365), [`core/dr-pipeline/src/framing.rs:927`](../core/dr-pipeline/src/framing.rs#L927), [`core/dr-types/src/lib.rs:336`](../core/dr-types/src/lib.rs#L336), [`core/dr-types/src/lib.rs:444`](../core/dr-types/src/lib.rs#L444), [`core/dr-types/src/lib.rs:456`](../core/dr-types/src/lib.rs#L456), [`core/dr-types/src/lib.rs:472`](../core/dr-types/src/lib.rs#L472), [`ui/dr-ui/src/develop.rs:138`](../ui/dr-ui/src/develop.rs#L138), [`ui/dr-ui/src/develop.rs:1975`](../ui/dr-ui/src/develop.rs#L1975), [`ui/dr-ui/src/segmentation.rs:322`](../ui/dr-ui/src/segmentation.rs#L322) |
|
||||||
| 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-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:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:2856`](../ui/dr-ui/src/develop.rs#L2856), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/develop.rs:3369`](../ui/dr-ui/src/develop.rs#L3369), [`ui/dr-ui/src/develop.rs:3382`](../ui/dr-ui/src/develop.rs#L3382), [`ui/dr-ui/src/develop.rs:3394`](../ui/dr-ui/src/develop.rs#L3394), [`ui/dr-ui/src/develop.rs:3410`](../ui/dr-ui/src/develop.rs#L3410), [`ui/dr-ui/src/develop.rs:3442`](../ui/dr-ui/src/develop.rs#L3442), [`ui/dr-ui/src/develop.rs:3446`](../ui/dr-ui/src/develop.rs#L3446), [`ui/dr-ui/src/develop.rs:3465`](../ui/dr-ui/src/develop.rs#L3465), [`ui/dr-ui/src/develop.rs:3481`](../ui/dr-ui/src/develop.rs#L3481), [`ui/dr-ui/src/develop.rs:610`](../ui/dr-ui/src/develop.rs#L610), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1386`](../ui/dr-ui/src/lib.rs#L1386), [`ui/dr-ui/src/lib.rs:1400`](../ui/dr-ui/src/lib.rs#L1400), [`ui/dr-ui/src/lib.rs:1408`](../ui/dr-ui/src/lib.rs#L1408), [`ui/dr-ui/src/lib.rs:2294`](../ui/dr-ui/src/lib.rs#L2294), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
| FR-DEV-5 | [`core/dr-pipeline/src/graph.rs:345`](../core/dr-pipeline/src/graph.rs#L345), [`core/dr-pipeline/src/graph.rs:384`](../core/dr-pipeline/src/graph.rs#L384), [`core/dr-pipeline/src/history.rs:102`](../core/dr-pipeline/src/history.rs#L102), [`core/dr-pipeline/src/history.rs:110`](../core/dr-pipeline/src/history.rs#L110), [`core/dr-pipeline/src/history.rs:127`](../core/dr-pipeline/src/history.rs#L127), [`core/dr-pipeline/src/history.rs:184`](../core/dr-pipeline/src/history.rs#L184), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:234`](../core/dr-pipeline/src/history.rs#L234), [`core/dr-pipeline/src/history.rs:293`](../core/dr-pipeline/src/history.rs#L293), [`core/dr-pipeline/src/history.rs:479`](../core/dr-pipeline/src/history.rs#L479), [`core/dr-pipeline/src/history.rs:489`](../core/dr-pipeline/src/history.rs#L489), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`core/dr-pipeline/src/history.rs:86`](../core/dr-pipeline/src/history.rs#L86), [`core/dr-pipeline/src/state.rs:1`](../core/dr-pipeline/src/state.rs#L1), [`core/dr-pipeline/src/state.rs:75`](../core/dr-pipeline/src/state.rs#L75), [`ui/dr-ui/src/develop.rs:2856`](../ui/dr-ui/src/develop.rs#L2856), [`ui/dr-ui/src/develop.rs:3339`](../ui/dr-ui/src/develop.rs#L3339), [`ui/dr-ui/src/develop.rs:3369`](../ui/dr-ui/src/develop.rs#L3369), [`ui/dr-ui/src/develop.rs:3382`](../ui/dr-ui/src/develop.rs#L3382), [`ui/dr-ui/src/develop.rs:3394`](../ui/dr-ui/src/develop.rs#L3394), [`ui/dr-ui/src/develop.rs:3410`](../ui/dr-ui/src/develop.rs#L3410), [`ui/dr-ui/src/develop.rs:3442`](../ui/dr-ui/src/develop.rs#L3442), [`ui/dr-ui/src/develop.rs:3446`](../ui/dr-ui/src/develop.rs#L3446), [`ui/dr-ui/src/develop.rs:3465`](../ui/dr-ui/src/develop.rs#L3465), [`ui/dr-ui/src/develop.rs:3481`](../ui/dr-ui/src/develop.rs#L3481), [`ui/dr-ui/src/develop.rs:610`](../ui/dr-ui/src/develop.rs#L610), [`ui/dr-ui/src/labels.rs:12`](../ui/dr-ui/src/labels.rs#L12), [`ui/dr-ui/src/labels.rs:215`](../ui/dr-ui/src/labels.rs#L215), [`ui/dr-ui/src/lib.rs:1386`](../ui/dr-ui/src/lib.rs#L1386), [`ui/dr-ui/src/lib.rs:1400`](../ui/dr-ui/src/lib.rs#L1400), [`ui/dr-ui/src/lib.rs:1408`](../ui/dr-ui/src/lib.rs#L1408), [`ui/dr-ui/src/lib.rs:2294`](../ui/dr-ui/src/lib.rs#L2294), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
||||||
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:3281`](../ui/dr-ui/src/develop.rs#L3281), [`ui/dr-ui/src/develop.rs:3302`](../ui/dr-ui/src/develop.rs#L3302), [`ui/dr-ui/src/lib.rs:1365`](../ui/dr-ui/src/lib.rs#L1365), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`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:547`](../ui/dr-ui/src/settings_ui.rs#L547), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1315`](../ui/dr-ui/ui/library.slint#L1315), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:913`](../ui/dr-ui/ui/library.slint#L913), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100) |
|
| 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:3281`](../ui/dr-ui/src/develop.rs#L3281), [`ui/dr-ui/src/develop.rs:3302`](../ui/dr-ui/src/develop.rs#L3302), [`ui/dr-ui/src/lib.rs:1365`](../ui/dr-ui/src/lib.rs#L1365), [`ui/dr-ui/src/library.rs:1551`](../ui/dr-ui/src/library.rs#L1551), [`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:547`](../ui/dr-ui/src/settings_ui.rs#L547), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1315`](../ui/dr-ui/ui/library.slint#L1315), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:913`](../ui/dr-ui/ui/library.slint#L913), [`ui/dr-ui/ui/settings.slint:100`](../ui/dr-ui/ui/settings.slint#L100) |
|
||||||
| FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:3410`](../ui/dr-ui/src/develop.rs#L3410), [`ui/dr-ui/src/develop.rs:3442`](../ui/dr-ui/src/develop.rs#L3442), [`ui/dr-ui/src/lib.rs:1408`](../ui/dr-ui/src/lib.rs#L1408), [`ui/dr-ui/src/lib.rs:2294`](../ui/dr-ui/src/lib.rs#L2294), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
| FR-DEV-7 | [`core/dr-pipeline/src/history.rs:214`](../core/dr-pipeline/src/history.rs#L214), [`core/dr-pipeline/src/history.rs:499`](../core/dr-pipeline/src/history.rs#L499), [`core/dr-pipeline/src/history.rs:526`](../core/dr-pipeline/src/history.rs#L526), [`ui/dr-ui/src/develop.rs:3410`](../ui/dr-ui/src/develop.rs#L3410), [`ui/dr-ui/src/develop.rs:3442`](../ui/dr-ui/src/develop.rs#L3442), [`ui/dr-ui/src/lib.rs:1408`](../ui/dr-ui/src/lib.rs#L1408), [`ui/dr-ui/src/lib.rs:2294`](../ui/dr-ui/src/lib.rs#L2294), [`ui/dr-ui/ui/history.slint:1`](../ui/dr-ui/ui/history.slint#L1) |
|
||||||
| 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:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:681`](../core/dr-pipeline/src/graph.rs#L681), [`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:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2127`](../ui/dr-ui/src/develop.rs#L2127), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/develop.rs:2230`](../ui/dr-ui/src/develop.rs#L2230), [`ui/dr-ui/src/develop.rs:2313`](../ui/dr-ui/src/develop.rs#L2313), [`ui/dr-ui/src/develop.rs:2327`](../ui/dr-ui/src/develop.rs#L2327), [`ui/dr-ui/src/develop.rs:658`](../ui/dr-ui/src/develop.rs#L658), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:2391`](../ui/dr-ui/src/lib.rs#L2391), [`ui/dr-ui/src/lib.rs:306`](../ui/dr-ui/src/lib.rs#L306), [`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:1835`](../ui/dr-ui/ui/app.slint#L1835), [`ui/dr-ui/ui/app.slint:2352`](../ui/dr-ui/ui/app.slint#L2352), [`ui/dr-ui/ui/app.slint:2618`](../ui/dr-ui/ui/app.slint#L2618), [`ui/dr-ui/ui/app.slint:332`](../ui/dr-ui/ui/app.slint#L332), [`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-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:113`](../core/dr-pipeline/src/graph.rs#L113), [`core/dr-pipeline/src/graph.rs:191`](../core/dr-pipeline/src/graph.rs#L191), [`core/dr-pipeline/src/graph.rs:685`](../core/dr-pipeline/src/graph.rs#L685), [`core/dr-pipeline/src/operation.rs:330`](../core/dr-pipeline/src/operation.rs#L330), [`core/dr-pipeline/src/operation.rs:554`](../core/dr-pipeline/src/operation.rs#L554), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:672`](../core/dr-pipeline/src/sidecar.rs#L672), [`core/dr-pipeline/src/sidecar.rs:808`](../core/dr-pipeline/src/sidecar.rs#L808), [`core/dr-pipeline/src/sidecar.rs:862`](../core/dr-pipeline/src/sidecar.rs#L862), [`core/dr-pipeline/src/sidecar.rs:892`](../core/dr-pipeline/src/sidecar.rs#L892), [`core/dr-pipeline/src/spot.rs:115`](../core/dr-pipeline/src/spot.rs#L115), [`core/dr-pipeline/src/spot.rs:151`](../core/dr-pipeline/src/spot.rs#L151), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:207`](../core/dr-pipeline/src/spot.rs#L207), [`core/dr-pipeline/src/spot.rs:387`](../core/dr-pipeline/src/spot.rs#L387), [`core/dr-pipeline/src/spot.rs:472`](../core/dr-pipeline/src/spot.rs#L472), [`core/dr-pipeline/src/spot.rs:582`](../core/dr-pipeline/src/spot.rs#L582), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`core/dr-pipeline/src/state.rs:103`](../core/dr-pipeline/src/state.rs#L103), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1), [`ui/dr-ui/src/develop.rs:2127`](../ui/dr-ui/src/develop.rs#L2127), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/develop.rs:2230`](../ui/dr-ui/src/develop.rs#L2230), [`ui/dr-ui/src/develop.rs:2313`](../ui/dr-ui/src/develop.rs#L2313), [`ui/dr-ui/src/develop.rs:2327`](../ui/dr-ui/src/develop.rs#L2327), [`ui/dr-ui/src/develop.rs:658`](../ui/dr-ui/src/develop.rs#L658), [`ui/dr-ui/src/labels.rs:52`](../ui/dr-ui/src/labels.rs#L52), [`ui/dr-ui/src/lib.rs:1429`](../ui/dr-ui/src/lib.rs#L1429), [`ui/dr-ui/src/lib.rs:2391`](../ui/dr-ui/src/lib.rs#L2391), [`ui/dr-ui/src/lib.rs:306`](../ui/dr-ui/src/lib.rs#L306), [`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:1835`](../ui/dr-ui/ui/app.slint#L1835), [`ui/dr-ui/ui/app.slint:2352`](../ui/dr-ui/ui/app.slint#L2352), [`ui/dr-ui/ui/app.slint:2618`](../ui/dr-ui/ui/app.slint#L2618), [`ui/dr-ui/ui/app.slint:332`](../ui/dr-ui/ui/app.slint#L332), [`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:543`](../core/dr-pipeline/src/graph.rs#L543), [`core/dr-pipeline/src/graph.rs:573`](../core/dr-pipeline/src/graph.rs#L573), [`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:2627`](../ui/dr-ui/src/develop.rs#L2627), [`ui/dr-ui/src/develop.rs:3640`](../ui/dr-ui/src/develop.rs#L3640), [`ui/dr-ui/src/develop.rs:4123`](../ui/dr-ui/src/develop.rs#L4123), [`ui/dr-ui/src/develop.rs:4157`](../ui/dr-ui/src/develop.rs#L4157), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/lib.rs:736`](../ui/dr-ui/src/lib.rs#L736), [`ui/dr-ui/src/lib.rs:795`](../ui/dr-ui/src/lib.rs#L795) |
|
| 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:547`](../core/dr-pipeline/src/graph.rs#L547), [`core/dr-pipeline/src/graph.rs:577`](../core/dr-pipeline/src/graph.rs#L577), [`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:657`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L657), [`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:672`](../core/dr-pipeline/src/ops/local_contrast.rs#L672), [`core/dr-pipeline/src/ops/noise_reduction.rs:698`](../core/dr-pipeline/src/ops/noise_reduction.rs#L698), [`core/dr-pipeline/src/spot.rs:673`](../core/dr-pipeline/src/spot.rs#L673), [`ui/dr-ui/src/develop.rs:2627`](../ui/dr-ui/src/develop.rs#L2627), [`ui/dr-ui/src/develop.rs:3640`](../ui/dr-ui/src/develop.rs#L3640), [`ui/dr-ui/src/develop.rs:4123`](../ui/dr-ui/src/develop.rs#L4123), [`ui/dr-ui/src/develop.rs:4157`](../ui/dr-ui/src/develop.rs#L4157), [`ui/dr-ui/src/lib.rs:70`](../ui/dr-ui/src/lib.rs#L70), [`ui/dr-ui/src/lib.rs:736`](../ui/dr-ui/src/lib.rs#L736), [`ui/dr-ui/src/lib.rs:795`](../ui/dr-ui/src/lib.rs#L795) |
|
||||||
| 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-6 | [`core/dr-pipeline/src/operation.rs:483`](../core/dr-pipeline/src/operation.rs#L483), [`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:2693`](../ui/dr-ui/src/develop.rs#L2693), [`ui/dr-ui/src/develop.rs:5225`](../ui/dr-ui/src/develop.rs#L5225), [`ui/dr-ui/src/develop.rs:5257`](../ui/dr-ui/src/develop.rs#L5257), [`ui/dr-ui/src/develop.rs:621`](../ui/dr-ui/src/develop.rs#L621), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1485`](../ui/dr-ui/src/lib.rs#L1485), [`ui/dr-ui/src/lib.rs:296`](../ui/dr-ui/src/lib.rs#L296), [`ui/dr-ui/ui/app.slint:292`](../ui/dr-ui/ui/app.slint#L292), [`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:2693`](../ui/dr-ui/src/develop.rs#L2693), [`ui/dr-ui/src/develop.rs:5225`](../ui/dr-ui/src/develop.rs#L5225), [`ui/dr-ui/src/develop.rs:5257`](../ui/dr-ui/src/develop.rs#L5257), [`ui/dr-ui/src/develop.rs:621`](../ui/dr-ui/src/develop.rs#L621), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1485`](../ui/dr-ui/src/lib.rs#L1485), [`ui/dr-ui/src/lib.rs:296`](../ui/dr-ui/src/lib.rs#L296), [`ui/dr-ui/ui/app.slint:292`](../ui/dr-ui/ui/app.slint#L292), [`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-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:533`](../core/dr-pipeline/src/graph.rs#L533), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`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-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:537`](../core/dr-pipeline/src/graph.rs#L537), [`core/dr-pipeline/src/graph.rs:591`](../core/dr-pipeline/src/graph.rs#L591), [`core/dr-pipeline/src/operation.rs:483`](../core/dr-pipeline/src/operation.rs#L483), [`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-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-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-5 | [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
|
||||||
@@ -97,10 +97,12 @@ _None._
|
|||||||
| 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:1104`](../ui/dr-ui/src/lib.rs#L1104), [`ui/dr-ui/ui/import.slint:5`](../ui/dr-ui/ui/import.slint#L5) |
|
| 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:1104`](../ui/dr-ui/src/lib.rs#L1104), [`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:1104`](../ui/dr-ui/src/lib.rs#L1104) |
|
| 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:1104`](../ui/dr-ui/src/lib.rs#L1104) |
|
||||||
| FR-NC-8 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1738`](../ui/dr-ui/src/lib.rs#L1738), [`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-8 | [`core/dr-pipeline/src/sidecar.rs:118`](../core/dr-pipeline/src/sidecar.rs#L118), [`core/dr-pipeline/src/sidecar.rs:92`](../core/dr-pipeline/src/sidecar.rs#L92), [`ui/dr-ui/src/lib.rs:1738`](../ui/dr-ui/src/lib.rs#L1738), [`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:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2024`](../core/dr-pipeline/src/sidecar.rs#L2024), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`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-NC-9 | [`core/dr-catalog/src/merge.rs:1`](../core/dr-catalog/src/merge.rs#L1), [`core/dr-catalog/src/schema.rs:498`](../core/dr-catalog/src/schema.rs#L498), [`core/dr-catalog/src/sync.rs:1`](../core/dr-catalog/src/sync.rs#L1), [`core/dr-pipeline/src/sidecar.rs:156`](../core/dr-pipeline/src/sidecar.rs#L156), [`core/dr-pipeline/src/sidecar.rs:183`](../core/dr-pipeline/src/sidecar.rs#L183), [`core/dr-pipeline/src/sidecar.rs:2033`](../core/dr-pipeline/src/sidecar.rs#L2033), [`core/dr-pipeline/src/sidecar.rs:352`](../core/dr-pipeline/src/sidecar.rs#L352), [`core/dr-pipeline/src/sidecar.rs:450`](../core/dr-pipeline/src/sidecar.rs#L450), [`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-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-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:845`](../ui/dr-ui/src/lib.rs#L845), [`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:845`](../ui/dr-ui/src/lib.rs#L845), [`ui/dr-ui/src/settings_store.rs:1`](../ui/dr-ui/src/settings_store.rs#L1) |
|
||||||
|
| FR-PLG-2 | [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/expr.rs:152`](../core/dr-pipeline/src/declared/expr.rs#L152), [`core/dr-pipeline/src/declared/expr.rs:1`](../core/dr-pipeline/src/declared/expr.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:82`](../core/dr-pipeline/src/declared/mod.rs#L82), [`core/dr-pipeline/src/descriptor.rs:15`](../core/dr-pipeline/src/descriptor.rs#L15), [`core/dr-pipeline/src/descriptor.rs:635`](../core/dr-pipeline/src/descriptor.rs#L635), [`core/dr-pipeline/src/operation.rs:232`](../core/dr-pipeline/src/operation.rs#L232) |
|
||||||
|
| FR-PLG-2d | [`core/dr-pipeline/src/declared/decl.rs:112`](../core/dr-pipeline/src/declared/decl.rs#L112), [`core/dr-pipeline/src/declared/decl.rs:152`](../core/dr-pipeline/src/declared/decl.rs#L152), [`core/dr-pipeline/src/declared/decl.rs:1`](../core/dr-pipeline/src/declared/decl.rs#L1), [`core/dr-pipeline/src/declared/decl.rs:414`](../core/dr-pipeline/src/declared/decl.rs#L414), [`core/dr-pipeline/src/declared/decl.rs:67`](../core/dr-pipeline/src/declared/decl.rs#L67), [`core/dr-pipeline/src/declared/mod.rs:1`](../core/dr-pipeline/src/declared/mod.rs#L1), [`core/dr-pipeline/src/declared/mod.rs:384`](../core/dr-pipeline/src/declared/mod.rs#L384), [`core/dr-pipeline/src/declared/mod.rs:403`](../core/dr-pipeline/src/declared/mod.rs#L403) |
|
||||||
| FR-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-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-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:186`](../ui/dr-ui/src/lib.rs#L186) |
|
| FR-RAW-4 | [`core/dr-decode/src/error.rs:1`](../core/dr-decode/src/error.rs#L1), [`ui/dr-ui/src/lib.rs:186`](../ui/dr-ui/src/lib.rs#L186) |
|
||||||
@@ -110,7 +112,7 @@ _None._
|
|||||||
| FR-UI-3 | [`ui/dr-ui/src/develop.rs:2056`](../ui/dr-ui/src/develop.rs#L2056), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/library_ui.rs:4385`](../ui/dr-ui/src/library_ui.rs#L4385), [`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:2100`](../ui/dr-ui/ui/app.slint#L2100), [`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-3 | [`ui/dr-ui/src/develop.rs:2056`](../ui/dr-ui/src/develop.rs#L2056), [`ui/dr-ui/src/develop.rs:2165`](../ui/dr-ui/src/develop.rs#L2165), [`ui/dr-ui/src/library_ui.rs:4385`](../ui/dr-ui/src/library_ui.rs#L4385), [`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:2100`](../ui/dr-ui/ui/app.slint#L2100), [`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:4385`](../ui/dr-ui/src/library_ui.rs#L4385), [`ui/dr-ui/src/library_ui.rs:4430`](../ui/dr-ui/src/library_ui.rs#L4430), [`ui/dr-ui/src/library_ui.rs:4533`](../ui/dr-ui/src/library_ui.rs#L4533), [`ui/dr-ui/src/library_ui.rs:4561`](../ui/dr-ui/src/library_ui.rs#L4561), [`ui/dr-ui/src/library_ui.rs:5273`](../ui/dr-ui/src/library_ui.rs#L5273), [`ui/dr-ui/src/library_ui.rs:5285`](../ui/dr-ui/src/library_ui.rs#L5285), [`ui/dr-ui/ui/app.slint:1772`](../ui/dr-ui/ui/app.slint#L1772), [`ui/dr-ui/ui/app.slint:650`](../ui/dr-ui/ui/app.slint#L650), [`ui/dr-ui/ui/app.slint:681`](../ui/dr-ui/ui/app.slint#L681), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) |
|
| 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:4385`](../ui/dr-ui/src/library_ui.rs#L4385), [`ui/dr-ui/src/library_ui.rs:4430`](../ui/dr-ui/src/library_ui.rs#L4430), [`ui/dr-ui/src/library_ui.rs:4533`](../ui/dr-ui/src/library_ui.rs#L4533), [`ui/dr-ui/src/library_ui.rs:4561`](../ui/dr-ui/src/library_ui.rs#L4561), [`ui/dr-ui/src/library_ui.rs:5273`](../ui/dr-ui/src/library_ui.rs#L5273), [`ui/dr-ui/src/library_ui.rs:5285`](../ui/dr-ui/src/library_ui.rs#L5285), [`ui/dr-ui/ui/app.slint:1772`](../ui/dr-ui/ui/app.slint#L1772), [`ui/dr-ui/ui/app.slint:650`](../ui/dr-ui/ui/app.slint#L650), [`ui/dr-ui/ui/app.slint:681`](../ui/dr-ui/ui/app.slint#L681), [`ui/dr-ui/ui/library.slint:1196`](../ui/dr-ui/ui/library.slint#L1196), [`ui/dr-ui/ui/library.slint:1203`](../ui/dr-ui/ui/library.slint#L1203), [`ui/dr-ui/ui/library.slint:1209`](../ui/dr-ui/ui/library.slint#L1209), [`ui/dr-ui/ui/library.slint:2278`](../ui/dr-ui/ui/library.slint#L2278), [`ui/dr-ui/ui/library.slint:823`](../ui/dr-ui/ui/library.slint#L823), [`ui/dr-ui/ui/library.slint:873`](../ui/dr-ui/ui/library.slint#L873), [`ui/dr-ui/ui/library.slint:892`](../ui/dr-ui/ui/library.slint#L892) |
|
||||||
| 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:2356`](../ui/dr-ui/src/lib.rs#L2356), [`ui/dr-ui/src/lib.rs:2763`](../ui/dr-ui/src/lib.rs#L2763), [`ui/dr-ui/src/lib.rs:2948`](../ui/dr-ui/src/lib.rs#L2948), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:313`](../ui/dr-ui/ui/app.slint#L313), [`ui/dr-ui/ui/collections.slint:4`](../ui/dr-ui/ui/collections.slint#L4) |
|
| 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:2356`](../ui/dr-ui/src/lib.rs#L2356), [`ui/dr-ui/src/lib.rs:2763`](../ui/dr-ui/src/lib.rs#L2763), [`ui/dr-ui/src/lib.rs:2948`](../ui/dr-ui/src/lib.rs#L2948), [`ui/dr-ui/src/masks_ui.rs:863`](../ui/dr-ui/src/masks_ui.rs#L863), [`ui/dr-ui/ui/app.slint:313`](../ui/dr-ui/ui/app.slint#L313), [`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) |
|
| FR-UI-7 | [`core/dr-pipeline/src/descriptor.rs:177`](../core/dr-pipeline/src/descriptor.rs#L177), [`core/dr-pipeline/src/framing.rs:262`](../core/dr-pipeline/src/framing.rs#L262) |
|
||||||
| NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) |
|
| NFR-ARCH-2 | [`core/dr-catalog/src/jobs.rs:1`](../core/dr-catalog/src/jobs.rs#L1), [`ui/dr-ui/src/faces.rs:1`](../ui/dr-ui/src/faces.rs#L1) |
|
||||||
| 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:2074`](../ui/dr-ui/src/lib.rs#L2074), [`ui/dr-ui/ui/app.slint:1128`](../ui/dr-ui/ui/app.slint#L1128), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) |
|
| 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:2074`](../ui/dr-ui/src/lib.rs#L2074), [`ui/dr-ui/ui/app.slint:1128`](../ui/dr-ui/ui/app.slint#L1128), [`ui/dr-ui/ui/library.slint:928`](../ui/dr-ui/ui/library.slint#L928) |
|
||||||
| 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-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) |
|
||||||
@@ -134,7 +136,7 @@ _None._
|
|||||||
|
|
||||||
## Not yet tagged
|
## Not yet tagged
|
||||||
|
|
||||||
79 of 177 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
|
77 of 177 requirements have no implementation tag. Expected while the codebase is young; each should gain one as it is built.
|
||||||
|
|
||||||
<details><summary>Show untagged requirements</summary>
|
<details><summary>Show untagged requirements</summary>
|
||||||
|
|
||||||
@@ -162,11 +164,9 @@ _None._
|
|||||||
- FR-PLG-11
|
- FR-PLG-11
|
||||||
- FR-PLG-12
|
- FR-PLG-12
|
||||||
- FR-PLG-1a
|
- FR-PLG-1a
|
||||||
- FR-PLG-2
|
|
||||||
- FR-PLG-2a
|
- FR-PLG-2a
|
||||||
- FR-PLG-2b
|
- FR-PLG-2b
|
||||||
- FR-PLG-2c
|
- FR-PLG-2c
|
||||||
- FR-PLG-2d
|
|
||||||
- FR-PLG-3
|
- FR-PLG-3
|
||||||
- FR-PLG-3a
|
- FR-PLG-3a
|
||||||
- FR-PLG-4
|
- FR-PLG-4
|
||||||
|
|||||||
+12
-12
@@ -1196,7 +1196,7 @@ fn curve_runs(op: &OpCapability, presentation: &Presentation) -> Option<Vec<Curv
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut runs: Vec<CurveRun> = Vec::new();
|
let mut runs: Vec<CurveRun> = Vec::new();
|
||||||
for id in presentation.params {
|
for id in &presentation.params {
|
||||||
// The widget addresses points by offset from the first of its run, so
|
// The widget addresses points by offset from the first of its run, so
|
||||||
// a run has to be contiguous in the capability list.
|
// a run has to be contiguous in the capability list.
|
||||||
let at = op.params.iter().position(|p| p.id == *id)?;
|
let at = op.params.iter().position(|p| p.id == *id)?;
|
||||||
@@ -4480,15 +4480,15 @@ mod tests {
|
|||||||
presentation: Some(Presentation {
|
presentation: Some(Presentation {
|
||||||
// Prefers a gradient handle; this frontend has none, so it
|
// Prefers a gradient handle; this frontend has none, so it
|
||||||
// falls back to the next entry, which the canvas does host.
|
// falls back to the next entry, which the canvas does host.
|
||||||
widgets: &[WidgetKind::GradientHandle, WidgetKind::CropOverlay],
|
widgets: vec![WidgetKind::GradientHandle, WidgetKind::CropOverlay],
|
||||||
demand: WidgetDemand {
|
demand: WidgetDemand {
|
||||||
two_dimensional: true,
|
two_dimensional: true,
|
||||||
precise_pointing: false,
|
precise_pointing: false,
|
||||||
},
|
},
|
||||||
params: &[ParamId("a"), ParamId("b")],
|
params: vec![ParamId("a"), ParamId("b")],
|
||||||
}),
|
}),
|
||||||
params: vec![param("a"), param("b")],
|
params: vec![param("a"), param("b")],
|
||||||
attributes: &[dr_pipeline::Attribute::Tone],
|
attributes: vec![dr_pipeline::Attribute::Tone],
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(rows_from(&[on_canvas]).is_empty());
|
assert!(rows_from(&[on_canvas]).is_empty());
|
||||||
@@ -4612,7 +4612,7 @@ mod tests {
|
|||||||
id: ParamId("method"),
|
id: ParamId("method"),
|
||||||
label: LocalizedKey("param.invented.method"),
|
label: LocalizedKey("param.invented.method"),
|
||||||
kind: ParamKind::Enum {
|
kind: ParamKind::Enum {
|
||||||
variants: &[
|
variants: vec![
|
||||||
LocalizedKey("param.invented.method.fast"),
|
LocalizedKey("param.invented.method.fast"),
|
||||||
LocalizedKey("param.invented.method.exact"),
|
LocalizedKey("param.invented.method.exact"),
|
||||||
],
|
],
|
||||||
@@ -4622,7 +4622,7 @@ mod tests {
|
|||||||
facet: None,
|
facet: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
attributes: &[dr_pipeline::Attribute::Tone],
|
attributes: vec![dr_pipeline::Attribute::Tone],
|
||||||
};
|
};
|
||||||
|
|
||||||
let rows = rows_from(&[invented]);
|
let rows = rows_from(&[invented]);
|
||||||
@@ -4660,12 +4660,12 @@ mod tests {
|
|||||||
label: LocalizedKey("op.grading"),
|
label: LocalizedKey("op.grading"),
|
||||||
active: false,
|
active: false,
|
||||||
presentation: Some(Presentation {
|
presentation: Some(Presentation {
|
||||||
widgets: &[WidgetKind::ColourWheel],
|
widgets: vec![WidgetKind::ColourWheel],
|
||||||
demand: WidgetDemand {
|
demand: WidgetDemand {
|
||||||
two_dimensional: true,
|
two_dimensional: true,
|
||||||
precise_pointing: false,
|
precise_pointing: false,
|
||||||
},
|
},
|
||||||
params: &[ParamId("hue"), ParamId("strength")],
|
params: vec![ParamId("hue"), ParamId("strength")],
|
||||||
}),
|
}),
|
||||||
params: vec![
|
params: vec![
|
||||||
ParamCapability {
|
ParamCapability {
|
||||||
@@ -4697,7 +4697,7 @@ mod tests {
|
|||||||
facet: None,
|
facet: None,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
attributes: &[dr_pipeline::Attribute::Tone],
|
attributes: vec![dr_pipeline::Attribute::Tone],
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(!supported(WidgetKind::ColourWheel), "precondition");
|
assert!(!supported(WidgetKind::ColourWheel), "precondition");
|
||||||
@@ -5146,15 +5146,15 @@ mod tests {
|
|||||||
label: LocalizedKey("op.invented_curve"),
|
label: LocalizedKey("op.invented_curve"),
|
||||||
active: false,
|
active: false,
|
||||||
presentation: Some(Presentation {
|
presentation: Some(Presentation {
|
||||||
widgets: &[WidgetKind::ToneCurve],
|
widgets: vec![WidgetKind::ToneCurve],
|
||||||
demand: WidgetDemand {
|
demand: WidgetDemand {
|
||||||
two_dimensional: true,
|
two_dimensional: true,
|
||||||
precise_pointing: true,
|
precise_pointing: true,
|
||||||
},
|
},
|
||||||
params: &IDS,
|
params: IDS.to_vec(),
|
||||||
}),
|
}),
|
||||||
params: IDS.iter().map(|id| param(*id)).collect(),
|
params: IDS.iter().map(|id| param(*id)).collect(),
|
||||||
attributes: &[dr_pipeline::Attribute::Tone],
|
attributes: vec![dr_pipeline::Attribute::Tone],
|
||||||
};
|
};
|
||||||
|
|
||||||
let presentation = plain.presentation.as_ref().expect("declares a widget");
|
let presentation = plain.presentation.as_ref().expect("declares a widget");
|
||||||
|
|||||||
Reference in New Issue
Block a user