# Develop nodes One file per operation. Adding a node to the pipeline is adding a file to this directory — there is no list to extend, no shader to edit, and no UI change. `../build.rs` compiles each declaration into Rust implementing [`Operation`](../src/operation.rs), generated into `OUT_DIR`. The result is indistinguishable downstream from a hand-written operation: the same `&'static OpDescriptor`, the same fused-shader composition, the same sidecar round-trip. ## What you get for free A node dropped in here arrives with: - **Controls.** The develop panel builds them from the parameters' declared kinds (FR-DEV-3c). It never learns the node's name. - **A place in the chain**, from `order:`. - **Persistence.** Parameters are ordinary scalars, so the sidecar writes them with everything else. - **Its tests**, compiled from `tests:` and run with `cargo test`. - **Neutral-means-absent.** At its defaults the node contributes no code, no uniform, and no branch to the generated shader. ## The shape of a file ```yaml id: exposure # must match the filename label: op.exposure # a localisation key, never a display string order: 20 # where it sits in the chain doc: | # becomes the generated module's documentation Exposure — a linear gain, expressed in stops. placement: | # why it sits where it does; appears above the A correction to capture. # entry in the generated chain params: exposure: label: param.exposure kind: stops min: -5 max: 5 doc: | # optional prose, kept with the descriptor ±5 stops. uniforms: gain: exp2(exposure) # shorthand, or a mapping with `value:` and `doc:` helpers: [luminance] # names from _helpers.yaml define: # helpers this node alone needs my_curve: | fn my_curve(x: f32) -> f32 { return x; } wgsl: | # `c` is linear RGB in and out c = c * gain; tests: - name: one_stop_is_a_doubling why: The definition of a stop. set: { exposure: 1 } expect: { gain: 2.0 } ``` ### Parameter kinds | `kind` | Shape | Extra keys | |---|---|---| | `amount` | −100…100, neutral at 0 — the familiar photographic control | — | | `stops` | exposure-like, in stops | `min`, `max` | | `fraction` | 0…1 | `default` | | `switch` | a toggle, neutral off | — | | `scalar` | anything else | `min`, `max`, `default`, `unit`, `scale`, `precision` | Reach for `amount` first. Writing its range out by hand in each node is how one of them comes to disagree with the rest. ### Uniform expressions Arithmetic (`+ - * /`), parentheses, numbers, this node's parameters, and: `exp2` `log2` `exp` `sqrt` `abs` `floor` `ceil` `round` `pow` `min` `max` `clamp` `mix`. Compiled to Rust, so an unknown name or a wrong arity is a build error naming the file and the key. Deliberately small: a node is a description, and letting it name arbitrary Rust would make this a second, worse place to write code. ### Activity By default a node is active exactly when some parameter has moved off its default, which is the honest rule and the right one for nearly everything. A node whose neutral is something else declares `active:` as an expression — non-zero means active. ### Tests | Key | Asserts | |---|---| | `set` | parameter values to apply first; build fails if out of range | | `expect` | uniform values, within 1e-5 | | `expect_range` | a uniform lies in `[low, high]` | | `expect_active` | whether the node reaches the shader | | `expect_wgsl` | substrings the fragment must contain | | `expect_helper_wgsl` | substrings a named helper must contain | `why:` becomes a comment in the generated test. Use it — a test named for a property should say what breaks without it. ## Hand-written nodes Some operations are not four facts, and forcing them into this schema would produce a worse language aimed at one caller. Those stay in [`../src/ops/`](../src/ops/) and declare only their position here: ```yaml id: tone_curve order: 60 rust: ToneCurve why_rust: | Its neutral is a relationship between five points, not a set of values. ``` They still belong in this directory, because the pipeline's **order** is the one thing a reader comes here to learn, and an order written half in YAML and half in Rust would be worse than either alone. Currently hand-written: `tone_curve` (a curve widget over five interpolated points), `colour_mixer` (thirty-six faceted parameters from twelve computed hue bands). `vignetting` is hand-written too but is not in the develop chain — it carries lens-profile coefficients that are not parameters. `distortion` and `aberration` are `Warp`s rather than operations: they rewrite coordinates before sampling rather than transforming a colour after it. ## Errors The build script reports failures by naming the key you got wrong, and exits rather than panicking, so the message is the message and not a backtrace. It refuses: a duplicate `order:`, a filename disagreeing with its `id:`, a default outside its own range, a test value the graph would clamp before the node saw it, a helper that does not define the function it names, an expression naming something that is not a parameter, and a declared node whose name collides with a file in `../src/ops/`.