An earlier commit claimed to move `film_sim` from `[tone, colour]` to `[effect]` and did not. It edited `ops/film_sim.yaml`, where `attributes:` is read, validated against the vocabulary, and then dropped: a `rust:` node publishes its own descriptor, and the type still said tone and colour. The stock went on appearing in the Light group beside exposure and again in Colour beside white balance, exactly as before, and every test passed. Nothing caught it because nothing could. The declaration parsed, the parity tests compare ids rather than attributes, and an operation filed under the wrong groups renders perfectly. It surfaced only on screen, as a missing Effects tab — which is indistinguishable from a category that genuinely has nothing in it, and is precisely how `Optics` looked for as long as it was empty. So three changes rather than one: `FilmSim`'s descriptor declares `Attribute::Effect`, which is the move the earlier commit described. `attributes:` joins the keys a `rust:` node may not carry, beside `params`, `uniforms`, `wgsl`, `helpers`, `define` and `label`. The rule was already written — "its descriptor comes from the type" — and attributes were the one field that slipped past it. A key that is silently ignored is worse than one that is rejected, because it reads as though it worked; the eight hand-written declarations lose a line that never did anything. And a test asserts that every attribute the chain carries reaches the tab strip. That is the property that was actually broken, and its failure mode is invisible from every direction: the controls exist, they are in the shader, and there is no way to filter to them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
41 lines
2.1 KiB
YAML
41 lines
2.1 KiB
YAML
# A hand-written node. `rust:` names the type in `crate::ops` that implements
|
|
# `Operation`; everything else about it — its descriptor, its parameters, its
|
|
# WGSL — comes from that type rather than from this file.
|
|
#
|
|
# It appears here anyway so that `ops/` lists the whole pipeline in order.
|
|
# A chain half-declared here and half-ordered in Rust would be worse than
|
|
# either alone: the order is the one thing a reader comes to this directory
|
|
# to learn.
|
|
id: tone_curve
|
|
order: 60
|
|
|
|
# Both, and this is the case the plural exists for: the 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.
|
|
rust: ToneCurve
|
|
|
|
why_rust: |
|
|
Four curves — master, red, green, blue — of five control points each,
|
|
presented as one widget, with an interpolator and a monotonicity guarantee
|
|
behind them. Its neutral is a *relationship* between parameters rather than
|
|
a set of values — the identity diagonal, on every channel — which is not
|
|
something the declarative `active:` rule can express; its `presentation()`
|
|
spans forty parameters rather than describing one; and its fragment is
|
|
*assembled* rather than written, because each curve reaches the shader only
|
|
when it has been moved off the diagonal. A declared node's `wgsl:` is one
|
|
fixed block of text, which is the right shape for nearly everything here and
|
|
the wrong one for a node whose cost has to follow what the photographer
|
|
actually touched.
|
|
|
|
placement: |
|
|
After the fixed-weight region controls, so the curve is the final word on
|
|
tone: a photographer reaches for it to fix what those controls could not
|
|
place exactly.
|
|
|
|
Within the node, the master curve runs before the per-channel ones. Both
|
|
orders are visibly different images and the reasons for this one are written
|
|
out in `src/ops/curve.rs`: the master is tonal and hue-preserving, the
|
|
per-channel curves are the chromatic grade over the tones it produced, and a
|
|
point placed on a channel curve should act on the tone the photographer can
|
|
see rather than on the one the master is about to move.
|