Let an operation say what it is about, so the panel can group without naming

Tool tabs need a taxonomy, and the taxonomy was the problem: a table in
`ui/` mapping operation to tab breaks FR-DEV-3a, and a `group:` field risks
what `ui-refinement.md` condemned `starts-group` for — the core deciding
where the panel draws things.

`Attribute` threads the needle. It says what an operation *is* — tone,
colour, detail, optics, geometry, effect — which is the same category as
`ParamKind` and squarely on the core's side of ARCH §4.3a's line. What is
drawn, where it sits and whether it is visible stay the frontend's. There is
no attribute for "the third tab", the enum's order is declaration order
rather than screen order, and a frontend may render these as tabs, as
headings, or ignore them.

The payoff is that a tab strip can be *derived*: the groups are the
attributes present in the capability list, so the interface names no
operation and needs no table to keep in step. An operation joins the right
group by declaring what it is, which is the one thing its author is well
placed to say.

Plural, because the tone curve is genuinely both — an RGB curve is tonal and
the per-channel curves are chromatic, and filing it under one would hide it
from half the people looking for it.

Required and non-empty, enforced in `build.rs`, and the failure was checked
by removing the line rather than assumed. An operation with no attribute is
invisible to a panel that groups by them; a build that stops costs ten
seconds, a control nobody can find costs more. The vocabulary is closed for
the same reason: a typo would otherwise invent a category holding exactly one
operation, which looks like a deliberate one until somebody counts.

Six tests over the real chain, including the hand-written operations that
`build.rs` never sees and so cannot check.
This commit is contained in:
2026-08-22 10:10:00 +02:00
parent acd694c2cf
commit 7421837c8a
26 changed files with 385 additions and 17 deletions
+2 -1
View File
@@ -32,7 +32,7 @@
//! least to perceived sharpness. Scaling all three about a virtual reference
//! would soften the image even when the correction is right.
use crate::descriptor::{LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit};
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit};
use crate::lens::Warp;
use crate::operation::{Helper, Uniform};
@@ -48,6 +48,7 @@ pub const BLUE: ParamId = ParamId("blue");
const MAX_SCALE: f32 = 0.005;
static DESCRIPTOR: OpDescriptor = OpDescriptor {
attributes: &[Attribute::Optics],
id: ID,
label: LocalizedKey("op.aberration"),
params: &[
+2 -1
View File
@@ -30,7 +30,7 @@
//! 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.
use crate::descriptor::{Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
use crate::descriptor::{Attribute, Facet, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
use crate::operation::{Helper, Operation, Uniform};
use crate::ops::helpers;
@@ -174,6 +174,7 @@ macro_rules! band_params {
// cannot be read out of `BANDS` here. `facets_match_their_bands` below is
// what keeps them from drifting.
static DESCRIPTOR: OpDescriptor = OpDescriptor {
attributes: &[Attribute::Colour],
id: ID,
label: LocalizedKey("op.colour_mixer"),
params: band_params![
+6 -4
View File
@@ -31,10 +31,8 @@
//! filter constrains the tangents so the interpolant is monotone wherever the
//! data is, which is exactly the guarantee a tone curve needs.
use crate::descriptor::{
LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation, Scale, Unit,
WidgetDemand, WidgetKind,
};
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Presentation, Scale, Unit,
WidgetDemand, WidgetKind,};
use crate::operation::{Helper, Operation, Uniform};
use crate::ops::helpers;
@@ -80,6 +78,10 @@ const fn coord(id: &'static str, label: &'static str, default: f32) -> ParamDesc
}
static DESCRIPTOR: OpDescriptor = OpDescriptor {
// Both, and this is the case the plural exists for: an RGB curve is
// tonal and the per-channel curves are chromatic. Filing it under one
// would hide it from half the people looking for it.
attributes: &[Attribute::Tone, Attribute::Colour],
id: ID,
label: LocalizedKey("op.tone_curve"),
// Defaults lie on y = x, so a fresh curve is the identity and the
+2 -1
View File
@@ -26,7 +26,7 @@
//! profile is a "make the horizon straight" task, which one term does well.
//! The full triple is reachable by loading a profile.
use crate::descriptor::{LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit};
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId, Scale, Unit};
use crate::lens::Warp;
use crate::operation::{Helper, Uniform};
@@ -34,6 +34,7 @@ pub const ID: OpId = OpId("distortion");
pub const AMOUNT: ParamId = ParamId("amount");
static DESCRIPTOR: OpDescriptor = OpDescriptor {
attributes: &[Attribute::Optics],
id: ID,
label: LocalizedKey("op.distortion"),
// ±100 maps to a ±0.25 cubic coefficient. That covers an uncorrected
+5 -1
View File
@@ -33,7 +33,7 @@
//! stages: a corner recovered by two stops has to be recovered while the
//! highlight headroom to hold it still exists (ARCH §5.2).
use crate::descriptor::{LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
use crate::descriptor::{Attribute, LocalizedKey, OpDescriptor, OpId, ParamDescriptor, ParamId};
use crate::operation::{Helper, Operation, Uniform};
pub const ID: OpId = OpId("vignetting");
@@ -46,6 +46,10 @@ pub const AMOUNT: ParamId = ParamId("amount");
const MAX_K1: f32 = -0.5;
static DESCRIPTOR: OpDescriptor = OpDescriptor {
// Optics rather than effect: this carries lens-profile coefficients
// and corrects what the lens did. A *creative* vignette is a different
// operation that does not exist yet, and would be `Effect`.
attributes: &[Attribute::Optics],
id: ID,
label: LocalizedKey("op.vignetting"),
// Bidirectional deliberately. Negative values *add* falloff, which is a