Skip to main content

rlx_core/render/scenes/
shape_field.rs

1//! **The mark roster, drawn at frame scale as a distance field**
2//! (ADR-0105, Plan 0091).
3//!
4//! Every other scene here hands the palette a *level* — a noise field, a
5//! chemical concentration, a particle's depth. This one hands it a **distance**,
6//! and that single substitution is the whole scene:
7//!
8//! ```text
9//! palette coordinate = mark_distance(p) * color_span + color_center
10//! ```
11//!
12//! (that is `coord_mode`'s default; the second coordinate is below.)
13//!
14//! Because a band of the palette coordinate is now a band of constant distance,
15//! turning `palette_steps` up produces **concentric offset contours of the
16//! chosen shape** — not concentric circles, and not an outline sampled to
17//! straight segments. It is per pixel, resolution-independent, and there is no
18//! geometry to facet (which is the entire argument of ADR-0105 against routing
19//! this through the line renderer). `palette_contour` then draws thin outlines
20//! at those band boundaries, and this is the third scene that param does
21//! anything in.
22//!
23//! # Two coordinates now, and the second is the one the references asked for
24//! (ADR-0111, Plan 0098)
25//!
26//! An offset family is an **erosion**, and erosion rounds a reflex corner while
27//! keeping convex ones sharp — so a nested heart keeps its bottom point and
28//! loses its top notch as the contours move inward, and no amount of tuning
29//! reaches the construction two batches of user reference images have asked for.
30//! `coord_mode = "1"` hands the palette
31//!
32//! ```text
33//! s = length(p) / r_boundary(theta)
34//! ```
35//!
36//! instead — `0` at the centre and exactly `1` on the outline, the same contract,
37//! but its level sets are **scaled copies** of the outline. The ring count is
38//! then `palette_steps` alone and the innermost figure is a scaled copy at any
39//! count, so notch sharpness stops trading against ring count.
40//!
41//! **The distance stays the default and stays bit-identical**, which is what
42//! keeps every shipped preset and every golden baseline on the arithmetic it has
43//! today. The two are not interchangeable settings of one knob: `color_span`
44//! means a different thing under each, because the exterior is divided by the
45//! shape's inradius under one and grows linearly in `r` under the other.
46//!
47//! # It shares the shape vocabulary rather than restating it
48//!
49//! The silhouettes come from `marks` — the same WGSL chunk
50//! `swarm` and `emitter` splice in, and the same CPU-side quantizers for the
51//! `shape` selector and the `points` count. So a mark a particle can wear and a
52//! figure this scene can be cannot drift apart, and the roster stays closed at
53//! five names (ADR-0084's consequence, restated in ADR-0105).
54//!
55//! # A silhouette can also be authored (ADR-0107)
56//!
57//! A `[path]` table hands this scene a closed contour parsed from inline SVG
58//! path data, and it takes the place of the roster's arm in exactly one spot:
59//! where the shader asks for the figure's coordinate. Everything downstream —
60//! both `coord_mode`s, `gamma`, the banding, the contours, the palette — is the
61//! same code reading the same `d`, so an authored figure gets the whole colour
62//! surface for free. The roster is untouched and still closed at five names; a
63//! preset naming no path executes not one instruction of the contour walk.
64//!
65//! Two things fall out of the distance being a **`min` over segments**. It is
66//! `O(N)` per pixel and the field is fullscreen, so the arity is a per-frame
67//! cost paid whether or not the figure is on screen — `MAX_SAMPLES` is where
68//! `core/tests/path_cost.rs` measured that becoming half the floor tier's frame
69//! budget, and asking for more is a load error. And **fill and stroke stop
70//! being two routes**: the interior is `d < 1` and the outline is
71//! `abs(d - 1) < w` on the one evaluation, which is what the `stroke` param is.
72//!
73//! What *is* new is that this scene reads the field **outside** the silhouette,
74//! where the particle path never looked. Plan 0091 Phase 2 measured that region
75//! and repaired the two arms that were wrong out there; see `marks.rs`'s own
76//! header for what it found and what it deliberately left approximate.
77//!
78//! # The aspect comes from the render target (ADR-0037)
79//!
80//! There is no internal grid here to take an aspect from by accident, which
81//! removes the usual mechanism for that bug but not the obligation. The figure
82//! is drawn in a square-unit space built by stretching NDC x by the **render
83//! target's** aspect, so a disc is round at every window shape. `tests` renders
84//! at 2:1 and 1:2 and measures the figure's own width against its height,
85//! because both 1920x1080 and this box's 2048x1152 quantize to exactly 16:9 —
86//! where no test can tell a right aspect source from a wrong one.
87
88// Hot-path panic-denial pragma, as everywhere under `scenes/`.
89#![deny(
90    clippy::unwrap_used,
91    clippy::expect_used,
92    clippy::indexing_slicing,
93    clippy::panic,
94    clippy::unreachable
95)]
96
97use crate::render::gpu;
98
99use super::Scene;
100use super::common;
101use super::marks;
102use crate::dsp::AnalysisFrame;
103use crate::preset::path::{MAX_ARC_PIECES, MAX_SAMPLES};
104use crate::render::palette::{self, Palette};
105use crate::render::scenes::{ParamGroup, ParamKind, ParamSpec, default_of};
106
107/// How many `vec4` one arc piece occupies: its circle, its sector test, its two
108/// endpoints, and its signed sweep. See the WGSL's `arc_chain_sd` for what each
109/// field is for.
110const VEC4S_PER_PIECE: usize = 4;
111
112/// How many `vec4` the uniform's geometry array holds — enough for
113/// [`MAX_ARC_PIECES`] arc pieces, which is more than the polyline's two points
114/// per element ever needs.
115///
116/// The geometry rides the **uniform** buffer rather than a storage one, and that
117/// is an ADR-0058 choice rather than a performance one: a fragment-visible
118/// read-only storage entry after this layout's uniform would make its shape
119/// byte-identical to `shape-collage-bind-layout`, which is live in the same
120/// frame during a preset dissolve. Two layouts of one shape alias on the DX12
121/// WARP adapter (a WARP reading, unverified on lavapipe as of 2026-10-06), and
122/// the whole golden suite captures on a software adapter, so the collision
123/// would be blessed rather than caught. Packing into the uniform leaves the
124/// layout's four entries exactly as they were.
125const PATH_VEC4S: usize = VEC4S_PER_PIECE * MAX_ARC_PIECES;
126
127/// The WGSL below spells the array length as a literal — `format!` cannot reach
128/// into a raw string full of braces — so the two are held together here. Raise
129/// either bound and this fails the build rather than letting the shader read
130/// past what the uniform carries.
131const _: () = assert!(
132    PATH_VEC4S == 128 && PATH_VEC4S >= MAX_SAMPLES / 2,
133    "the WGSL `path` array must be PATH_VEC4S long, and hold either geometry"
134);
135
136/// `scale` default — the figure's outline sits at 0.6 of the frame's short
137/// half-axis, which leaves room for several contour bands around it before they
138/// leave the frame. The whole point of this scene is what happens *outside* the
139/// silhouette, so a figure filling the frame would be the wrong default.
140const DEFAULT_SCALE: f32 = default_of(PARAMS, "scale");
141/// Smallest `scale` the shader is handed. Not zero: at zero the figure has no
142/// size and every pixel is infinitely far outside it in units of nothing, so
143/// the coordinate degenerates rather than fading out.
144const MIN_SCALE: f32 = 0.01;
145/// Largest `scale`. Past this the figure is far outside the frame and the whole
146/// screen is one interior band — reachable, but it is the end of the useful
147/// range rather than an arbitrary cap.
148const MAX_SCALE: f32 = 20.0;
149
150/// `rotation` default — **0, and an exact arithmetic identity**: the shader
151/// tests for it and skips the rotation entirely, so every shipped preset and
152/// every golden baseline stays on the arithmetic it has today.
153///
154/// Radians, matching `lines/star.rs` and `lines/lsystem.rs` — the two other
155/// figure-drawing scenes that carry this name. Unclamped for the same reason
156/// they are: an angle wraps, so there is no end of the useful range to hold it
157/// inside; a non-finite binding falls back to the identity because `cos(NaN)`
158/// would take the whole frame with it.
159const DEFAULT_ROTATION: f32 = default_of(PARAMS, "rotation");
160
161/// `stroke` default — **0, the filled figure, and an exact arithmetic
162/// identity**: the shader tests for it and skips the stroke mask entirely.
163const DEFAULT_STROKE: f32 = default_of(PARAMS, "stroke");
164/// Largest `stroke`. One coordinate unit is the figure's whole interior — 0 at
165/// its deepest point, 1 on the outline — so a half-width of 1 is a band
166/// reaching from the centre to twice the outline, and past that the stroke has
167/// stopped being an outline of anything.
168const MAX_STROKE: f32 = 1.0;
169
170/// `morph` default — **0, the authored figure**, and an exact identity: at 0 the
171/// packed contour is `[path] d` verbatim, with no interpolation run at all. A
172/// preset declaring no `morph_to` has nothing to travel towards and this is
173/// inert whatever it is bound to, exactly as the attractor's `morph` is.
174const DEFAULT_MORPH: f32 = default_of(PARAMS, "morph");
175
176/// `gamma` default — **the identity**, and it is exactly `1.0` on the way to the
177/// uniform because the shader's identity branch tests for it (`pow(x, 1.0)` is
178/// not bit-exact, ADR-0092's care).
179const DEFAULT_GAMMA: f32 = default_of(PARAMS, "gamma");
180/// The range `gamma` is held in. Same shape and the same reasoning as
181/// `ink_gamma` and `bg_ramp_gamma`: positive on both sides, wide enough that the
182/// clamp is the end of the useful range rather than a limit an author meets.
183const MIN_GAMMA: f32 = 0.05;
184const MAX_GAMMA: f32 = 20.0;
185
186/// The `coord_mode` roster, in the order the numeric parameter selects them.
187///
188/// `0` hands the palette the normalized **distance** to the figure, whose level
189/// sets are offset curves; `1` hands it `r / r_boundary(theta)`, whose level
190/// sets are **scaled copies** of the outline
191/// (ADR-0111). Both are `0` at the figure's centre and exactly `1` on its outline; what
192/// differs is the shape of everything in between.
193pub(crate) const COORD_MODES: [&str; 2] = ["distance", "radius"];
194
195/// `coord_mode` default — **0, the distance**, and that is an obligation rather
196/// than a preference: it is the arithmetic every shipped preset and every golden
197/// baseline has today.
198const DEFAULT_COORD_MODE: f32 = default_of(PARAMS, "coord_mode");
199const MIN_COORD_MODE: f32 = 0.0;
200const MAX_COORD_MODE: f32 = COORD_MODES.len() as f32 - 1.0;
201
202/// Shared palette colour knobs (ADR-0021). `color_span = 0.6` puts the
203/// silhouette's interior (`d` in `0..1`) across the gradient's first 60 %, so
204/// the exterior contours have somewhere to go.
205const DEFAULT_COLOR_SPAN: f32 = default_of(PARAMS, "color_span");
206const DEFAULT_COLOR_CENTER: f32 = default_of(PARAMS, "color_center");
207
208/// How many of the palette's [`LUT_SIZE`] texels a resting `color_span` spends on
209/// the **figure's own interior**.
210///
211/// Both coordinates are `0` at the figure's centre and exactly `1` on its
212/// outline, so the interior is one unit of the coordinate whatever the shape and
213/// whatever `gamma` does to the spacing inside it — which makes this share
214/// exact. What it does *not* know is how much of the frame that interior covers:
215/// a figure spanning half the screen stretches those texels across hundreds of
216/// pixels, and one filling a corner does not. That is why the warning built on
217/// it says estimate.
218pub(crate) fn interior_texels(color_span: f32) -> f32 {
219    color_span.abs() * crate::render::palette::LUT_SIZE as f32
220}
221
222/// Below this many texels across the interior, the linear-filtered LUT is
223/// stretching too few distinct colours across the figure and the result reads as
224/// an upscaled gradient rather than as shading (design-backlog 0099).
225///
226/// **The property is the count, not this constant.** A figure's interior drawn
227/// through N texels carries at most N colours no matter how large it is on
228/// screen, so somewhere below a few dozen the sampler is interpolating more than
229/// it is reading. The Plan 0091 Phase 6 star probes bracket where that becomes
230/// visible — 8.6 texels read as *"dirty and upscaled"*, 32.3 did not — and this
231/// is the middle of that bracket rounded to a power of two. It is a warning
232/// threshold, so a value inside it is legal and renders exactly as asked;
233/// `palette_steps` is the remedy, because a quantized coordinate samples one
234/// texel per band and interpolates nothing.
235pub(crate) const MIN_INTERIOR_TEXELS: f32 = 16.0;
236
237const SHADER: &str = r#"
238struct Params {
239    // x: aspect (from the RENDER TARGET), y: shape position (clamped CPU-side
240    // and NOT rounded, so a fractional value blends two arms - ADR-0226),
241    // z: points (quantized CPU-side), w: scale
242    a: vec4<f32>,
243    // xy: pan (the shared ViewTransform, ADR-0018), z: color_span,
244    // w: color_center
245    b: vec4<f32>,
246    // x: saturation, y: palette_mix, z: palette_steps (integral, quantized
247    // CPU-side), w: palette_contour
248    c: vec4<f32>,
249    // x: occlude (ADR-0085), y: gamma (the response exponent on the distance,
250    // exactly 1.0 for the identity), z: coord_mode (quantized CPU-side; 0 = the
251    // distance, 1 = the scaled-copy radius), w: rotation in radians, exactly 0.0
252    // for the identity.
253    d: vec4<f32>,
254    // xyz: the star arm's shape params (valley, curve, jitter), conditioned
255    // CPU-side. Inert on every other silhouette.
256    e: vec4<f32>,
257    // x: path point count (0 = no authored contour, and every line of the path
258    // arms below is unreached), y: the contour's inradius — the divisor that
259    // makes the distance 0 at its deepest interior point, measured CPU-side,
260    // z: stroke half-width in coordinate units (exactly 0.0 = filled),
261    // w: arc piece count — nonzero means `path` holds an ARC CHAIN rather than
262    // a polyline, and `x` is then unread.
263    f: vec4<f32>,
264    // x: palette_contour_style (integral, rounded CPU-side),
265    // y: palette_contour_ink (ADR-0197), zw: unused.
266    //
267    // Its own vec4 rather than `e.w` plus a slot borrowed from elsewhere: the two
268    // are one control split in half, and `e.w` is the only free slot this struct
269    // has.
270    g: vec4<f32>,
271    // The authored contour (ADR-0107), in one of two packings.
272    //
273    // **As a polyline** (`f.w == 0`): TWO POINTS PER ELEMENT, point `i` at
274    // `path[i >> 1].xy` for even `i` and `.zw` for odd. Packed because a uniform
275    // array's elements are 16-byte aligned, so an `array<vec2<f32>, N>` would
276    // spend half the buffer on padding.
277    //
278    // **As an arc chain** (`f.w > 0`): FOUR ELEMENTS PER PIECE, piece `i` at
279    // `path[i * 4 ..]`:
280    //   +0  (kind, cx, cy, radius)    kind 0 = straight run, 1 = arc
281    //   +1  (mx, my, cos_half, 0)     the sector's mid direction and half-angle
282    //   +2  (ax, ay, bx, by)          the piece's two endpoints
283    //   +3  (start, sweep, 0, 0)      the signed sweep, for the crossing test
284    path: array<vec4<f32>, 128>,
285}
286
287// **One bind group, sampler first and uniform last — and that arrangement is
288// what buys this pipeline a layout shape nothing else holds** (ADR-0058: two
289// byte-identical layouts alias on the DX12 WARP adapter, a WARP reading
290// unverified on lavapipe as of 2026-10-06, and the whole golden suite runs on a
291// software adapter, so a collision is blessed rather than caught).
292//
293// It is deliberately not `fragment_field`'s two-group split, because that split
294// has no free shape left for a tenth scene. A lone uniform group can vary only
295// by visibility and by whether it declares a `min_binding_size`, and all four
296// combinations are taken: `[Uniform:FRAGMENT]` by the fragment field, the RD
297// init and the test disc; `+size` by the backdrop; `VERTEX_FRAGMENT` by the
298// line renderer; and `VERTEX_FRAGMENT+size` by the emitter. Merging the groups
299// is what keeps this unique WITHOUT padding a layout with a binding the shader
300// does not use, which is the cure ADR-0058's Alternative A refuses.
301//
302// Pick another free shape rather than tidying this back into two groups.
303@group(0) @binding(0) var lut_samp: sampler;
304@group(0) @binding(1) var lut_a: texture_2d<f32>;
305@group(0) @binding(2) var lut_b: texture_2d<f32>;
306@group(0) @binding(3) var<uniform> params: Params;
307
308// Shared `saturation` (mirrors core/src/render/palette.rs::desaturate verbatim).
309fn apply_saturation(c: vec3<f32>, s: f32) -> vec3<f32> {
310    let luma = dot(c, vec3<f32>(0.299, 0.587, 0.114));
311    return vec3<f32>(luma) + (c - vec3<f32>(luma)) * s;
312}
313
314// Shared `palette_steps` (mirrors core/src/render/palette.rs::band_coord
315// verbatim, ADR-0078): snap the palette coordinate to a band centre before the
316// LUT read. Below 1.5 steps it is the exact identity, not a one-band degenerate.
317fn band_coord(t: f32, steps: f32) -> f32 {
318    if (steps < 1.5) {
319        return t;
320    }
321    return (floor(t * steps) + 0.5) / steps;
322}
323
324// Shared `palette_contour` (ADR-0078 / ADR-0133; the WGSL is the implementation,
325// copied verbatim at each fragment-stage site — palette.rs has no CPU
326// counterpart to be canonical, since `fwidth` exists only here).
327//
328// Darkens within one PIXEL of a band edge, so the line has the same weight where
329// the field is shallow and where it is steep — AND ONLY WHERE THE INK ACTUALLY
330// CHANGES (ADR-0133). It samples the two band centres either side of the nearest
331// edge and returns unchanged when they resolve to the same colour within half a
332// code value, which is below the LUT's own 8-bit quantization. On a smooth
333// palette two distinct centres always differ by at least one code value, so
334// every edge draws exactly as it did at any `palette_steps`; inside a plateau
335// the LUT is literally constant and the samples are bit-equal, so the line
336// vanishes there and survives at the run boundaries. One rule, both behaviours,
337// no new parameter.
338//
339// The two LUTs, the sampler and `palette_mix` are EXPLICIT parameters rather
340// than module-scope globals this happens to find: all six sites name them the
341// same today, so implicit capture would compile — and would silently bind the
342// shared function to whatever a future site called its textures.
343//
344// `textureSampleLevel`, not `textureSample`: the LUT has one mip, and an
345// explicit LOD keeps these reads free of the uniformity requirement that a
346// sample after a conditional return would otherwise carry.
347//
348// **What the line is drawn in is `style`** (ADR-0197): `0` the soft darkening
349// above, `1` a hard darkening over the same footprint, `2` a soft line in the
350// palette's own colour at `ink_t` and `3` a hard one. `style` arrives rounded to
351// a whole number from the CPU (`palette::band_contour_style`), so the equality
352// comparisons below are exact. The `style < 0.5` arm is the expression that
353// shipped before the other three existed, which is what keeps every golden still.
354fn band_contour_ink(
355    col: vec3<f32>,
356    t: f32,
357    steps: f32,
358    amount: f32,
359    style: f32,
360    ink_t: f32,
361    lut_a: texture_2d<f32>,
362    lut_b: texture_2d<f32>,
363    lut_samp: sampler,
364    mix_ab: f32,
365) -> vec3<f32> {
366    let f = t * steps;
367    let w = max(fwidth(f), 1e-5);
368    if (steps < 1.5 || amount <= 0.0) {
369        return col;
370    }
371    let n = round(f);
372    let m = clamp(mix_ab, 0.0, 1.0);
373    let lo = mix(
374        textureSampleLevel(lut_a, lut_samp, vec2<f32>((n - 0.5) / steps, 0.5), 0.0).rgb,
375        textureSampleLevel(lut_b, lut_samp, vec2<f32>((n - 0.5) / steps, 0.5), 0.0).rgb,
376        m
377    );
378    let hi = mix(
379        textureSampleLevel(lut_a, lut_samp, vec2<f32>((n + 0.5) / steps, 0.5), 0.0).rgb,
380        textureSampleLevel(lut_b, lut_samp, vec2<f32>((n + 0.5) / steps, 0.5), 0.0).rgb,
381        m
382    );
383    if (all(abs(hi - lo) < vec3<f32>(0.5 / 255.0))) {
384        return col;
385    }
386    let d = min(fract(f), 1.0 - fract(f));
387    if (style < 0.5) {
388        return col * (1.0 - clamp(amount, 0.0, 1.0) * (1.0 - smoothstep(0.0, w, d)));
389    }
390    let hard = style == 1.0 || style == 3.0;
391    let cover = select(1.0 - smoothstep(0.0, w, d), f32(d < w), hard);
392    let ink_lut = mix(
393        textureSampleLevel(lut_a, lut_samp, vec2<f32>(ink_t, 0.5), 0.0).rgb,
394        textureSampleLevel(lut_b, lut_samp, vec2<f32>(ink_t, 0.5), 0.0).rgb,
395        m
396    );
397    let ink = select(vec3<f32>(0.0), ink_lut, style >= 2.0);
398    return mix(col, ink, clamp(amount, 0.0, 1.0) * cover);
399}
400
401// Point `i` of the authored contour, unpacked from the two-per-vec4 array.
402fn path_pt(i: u32) -> vec2<f32> {
403    let v = params.path[i >> 1u];
404    if ((i & 1u) == 0u) {
405        return v.xy;
406    }
407    return v.zw;
408}
409
410// **The authored contour's signed distance**: `min` over the distance to each
411// closing segment, signed by a crossing count (ADR-0107).
412//
413// The sign is a RAY-CROSSING PARITY rather than an orientation test, so it does
414// not care which way the author wound their path — which is what lets Phase 1
415// keep the contour's own winding and leave the alignment to the morph.
416//
417// `min` over segment distances has no quads, no overlap and no vertex bead: it
418// is exactly correct at every join, which is why ADR-0098's faceting objection
419// against a polyline stroke does not transfer to a polyline FILL. The cost is
420// `O(n)` per pixel and it is paid at every pixel of the frame whether or not the
421// figure is on screen, which is what the arity ceiling exists to bound.
422fn path_sd(p: vec2<f32>, n: u32) -> f32 {
423    var best = 1e20;
424    var s = 1.0;
425    // The previous point is carried rather than re-indexed, so each iteration
426    // makes one dynamically indexed uniform read instead of two. It measured as
427    // free — `path_cost.rs` reports the same ms/frame either way, so the loop's
428    // cost is its arithmetic and not its loads — and it stays because it is the
429    // simpler loop, not because it bought anything.
430    var b = path_pt(n - 1u);
431    for (var i = 0u; i < n; i = i + 1u) {
432        let a = path_pt(i);
433        let e = b - a;
434        let w = p - a;
435        // The nearest point ON THE SEGMENT, not on its infinite line: the clamp
436        // is what makes a sample beyond an end measure to the vertex.
437        let t = clamp(dot(w, e) / max(dot(e, e), 1e-20), 0.0, 1.0);
438        let q = w - e * t;
439        best = min(best, dot(q, q));
440        let c1 = p.y >= a.y;
441        let c2 = p.y < b.y;
442        let c3 = e.x * w.y > e.y * w.x;
443        if ((c1 && c2 && c3) || (!c1 && !c2 && !c3)) {
444            s = -s;
445        }
446        b = a;
447    }
448    return s * sqrt(best);
449}
450
451// **The authored contour's signed distance, as a chain of circular arcs**
452// (ADR-0098's primitive, ADR-0107's figure).
453//
454// The same two quantities as `path_sd` — a `min` over pieces for the magnitude,
455// a ray-crossing parity for the sign — over a chain that a curve needs FIVE TO
456// TEN TIMES fewer of than the polyline it was fitted from. A piece costs more
457// than a segment; whether that trade is a win is `path_cost.rs`'s reading, not
458// an assertion here.
459//
460// **No `atan2` on the distance path.** Whether the nearest point on the circle
461// lies within the piece's sweep is a sector test, and a sector test is a dot
462// product against the sweep's mid direction — both precomputed CPU-side. The
463// crossing test below does need the angle, but only for a piece the scan line
464// actually meets, which is a small minority of them.
465fn arc_chain_sd(p: vec2<f32>, n: u32) -> f32 {
466    let TAU = 6.28318530718;
467    var best = 1e20;
468    var crossings = 0u;
469    for (var i = 0u; i < n; i = i + 1u) {
470        let base = i * 4u;
471        let head = params.path[base];
472        let ends = params.path[base + 2u];
473        let a = ends.xy;
474        let b = ends.zw;
475
476        if (head.x < 0.5) {
477            // A straight run — the fitter emits these for a corner it must keep
478            // and for an arc whose radius is too large to shade stably, so this
479            // arm carries a real share of a polygonal figure.
480            let e = b - a;
481            let w = p - a;
482            let t = clamp(dot(w, e) / max(dot(e, e), 1e-20), 0.0, 1.0);
483            let q = w - e * t;
484            best = min(best, dot(q, q));
485            // The half-open rule on y, exactly as the polyline uses it: a joint
486            // lying on the scan line belongs to one piece, not to both.
487            let c1 = p.y >= a.y;
488            let c2 = p.y < b.y;
489            let c3 = e.x * w.y > e.y * w.x;
490            if ((c1 && c2 && c3) || (!c1 && !c2 && !c3)) {
491                crossings = crossings + 1u;
492            }
493            continue;
494        }
495
496        let c = head.yz;
497        let r = head.w;
498        let sector = params.path[base + 1u];
499        let sweep = params.path[base + 3u];
500        let w = p - c;
501        let l = length(w);
502        // Inside the sweep, the nearest point on the circle is the nearest point
503        // on the arc; outside it, the nearest point is whichever end is closer.
504        if (l > 1e-9 && dot(w / l, sector.xy) >= sector.z) {
505            let d = abs(l - r);
506            best = min(best, d * d);
507        } else {
508            best = min(best, min(dot(p - a, p - a), dot(p - b, p - b)));
509        }
510
511        // The crossing test: where the scan line `y = p.y` meets this circle, to
512        // the RIGHT of `p`, and inside the sweep.
513        let dy = p.y - c.y;
514        let disc = r * r - dy * dy;
515        if (disc > 0.0) {
516            let sx = sqrt(disc);
517            for (var k = 0u; k < 2u; k = k + 1u) {
518                let xr = c.x + select(-sx, sx, k == 1u);
519                if (xr <= p.x) {
520                    continue;
521                }
522                // Half-open on the sweep — `u < span`, not `<=` — so a joint on
523                // the scan line is counted by the piece that starts there and
524                // not also by the one that ends there.
525                let ang = atan2(dy, xr - c.x);
526                var u = (ang - sweep.x) * sign(sweep.y);
527                u = u - TAU * floor(u / TAU);
528                if (u < abs(sweep.y)) {
529                    crossings = crossings + 1u;
530                }
531            }
532        }
533    }
534    return select(1.0, -1.0, (crossings & 1u) == 1u) * sqrt(best);
535}
536
537// The contour's radius along the ray from the figure's centre through `p` — the
538// divisor of `coord_mode = 1`'s scaled-copy coordinate (ADR-0111), on an
539// authored contour instead of a rostered arm.
540//
541// The OUTERMOST crossing is taken. A single closed contour that is star-shaped
542// about its centre has exactly one, and the choice only shows on one that is
543// not (a crescent), where the outer edge is the boundary and the concavity is
544// interior to the coordinate.
545fn path_boundary_radius(p: vec2<f32>, n: u32) -> f32 {
546    let l = length(p);
547    if (l < 1e-6) {
548        return 1e-6;
549    }
550    let u = p / l;
551    var r = 0.0;
552    var b = path_pt(n - 1u);
553    for (var i = 0u; i < n; i = i + 1u) {
554        let a = path_pt(i);
555        let e = b - a;
556        // Cross both sides of `s*u = a + e*t` with `u` to drop `s`, then solve
557        // for the segment parameter `t`.
558        let denom = e.x * u.y - e.y * u.x;
559        if (abs(denom) > 1e-9) {
560            let t = (a.y * u.x - a.x * u.y) / denom;
561            if (t >= 0.0 && t <= 1.0) {
562                let s = dot(a + e * t, u);
563                r = max(r, s);
564            }
565        }
566        b = a;
567    }
568    return max(r, 1e-6);
569}
570
571@fragment
572fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
573    let aspect = params.a.x;
574    let shape = params.a.y;
575    let points = params.a.z;
576    let scale = params.a.w;
577    let pan = params.b.xy;
578    let color_span = params.b.z;
579    let color_center = params.b.w;
580    let saturation = params.c.x;
581    let palette_mix = params.c.y;
582    let palette_steps = params.c.z;
583    let palette_contour = params.c.w;
584    let gamma = params.d.y;
585    let coord_mode = params.d.z;
586    let rotation = params.d.w;
587    let star = params.e.xyz;
588    // The star arm's hand-drawn controls (seed, wobble amplitude, wobble
589    // frequency), which live in the three padding slots `e` and `g` already
590    // carried rather than in a wider uniform.
591    let rough = vec3<f32>(params.e.w, params.g.z, params.g.w);
592    let path_n = u32(params.f.x);
593    let path_inradius = params.f.y;
594    let stroke = params.f.z;
595    let path_arcs = u32(params.f.w);
596    let palette_contour_style = params.g.x;
597    let palette_contour_ink = params.g.y;
598
599    // Square units, from the RENDER TARGET's aspect (ADR-0037): stretching x
600    // makes one unit of `uv` the same length on both axes, so the figure below
601    // is the shape it claims to be and not the window's shape.
602    var uv = in.ndc;
603    uv.x = uv.x * aspect;
604
605    // The figure's own frame: `pan` moves its centre, `scale` sets its size.
606    //
607    // **`rotation` is applied AFTER the pan, and that is a choice.** Turning the
608    // sample point before subtracting `pan` would swing the figure around the
609    // frame's centre — an orbit — and turning it after swings it about its own.
610    // Both are defensible and they look completely different; this scene draws
611    // ONE figure, and a figure that spins in place is what `rotation` means on
612    // `lines/star.rs` and `lines/lsystem.rs` too.
613    //
614    // It is done in `uv`, which is already SQUARE units (ADR-0037): x has been
615    // stretched by the render target's aspect, so one unit is the same length on
616    // both axes and this is a rotation. In raw NDC the same two lines would
617    // SHEAR — invisible at 16:9, where the stretch is nearly 1, and obvious at
618    // 2:1. `tests` renders a square at 2:1 and turns it a quarter turn.
619    //
620    // A branch rather than an unconditional multiply, so 0 is an exact identity
621    // and no shipped preset moves through `cos`/`sin` (ADR-0092's care, the same
622    // reason `gamma` has one).
623    var q = uv - pan;
624    if (rotation != 0.0) {
625        let cr = cos(rotation);
626        let sr = sin(rotation);
627        // The INVERSE rotation on the sample point, so a positive `rotation`
628        // turns the figure counter-clockwise on screen rather than the frame.
629        q = vec2<f32>(cr * q.x + sr * q.y, cr * q.y - sr * q.x);
630    }
631    let p = q / scale;
632
633    // THE substitution this scene exists for: the palette coordinate is a
634    // FIGURE coordinate rather than a level. Both modes are 0 at the figure's
635    // centre and exactly 1 on its outline, and both grow outward — what differs
636    // is what a band of the coordinate is a band OF.
637    //
638    // An `if` rather than a `select`, and that is not style: `select` evaluates
639    // both arms, and the second arm here is a whole second shape evaluation. The
640    // mode is a per-draw uniform, so this branch is uniform across a warp and
641    // the hardware takes one arm rather than both.
642    //
643    // An authored contour takes the same two modes on the same terms
644    // (ADR-0107): what changes is where the silhouette came from, not what a
645    // band of the coordinate is a band of. `path_n` is 0 for every preset that
646    // declares no `[path]`, so those take the roster arms below and not one
647    // instruction of the contour walk executes.
648    var d: f32;
649    if (path_arcs >= 1u) {
650        // The arc chain, chosen CPU-side and only where it can serve: the
651        // distance coordinate, and no morph in flight. `path_inradius` is the
652        // POLYLINE's, which describes the same figure to within the fit's own
653        // lateral budget — a sub-pixel difference in a divisor.
654        d = max(1.0 + arc_chain_sd(p, path_arcs) / max(path_inradius, 1e-6), 0.0);
655    } else if (path_n >= 3u) {
656        if (coord_mode < 0.5) {
657            // `1 + sd / inradius` — the SAME normalization `mark_distance`
658            // applies to the roster, so an authored figure reads 0 at its
659            // deepest interior point and exactly 1 on its outline like every
660            // other silhouette this scene draws. Held at 0 from below because
661            // the inradius is measured on a grid and can land a hair short of
662            // the true deepest point; a negative coordinate would be a NaN
663            // under a bound `gamma` (`pow` of a negative base).
664            d = max(1.0 + path_sd(p, path_n) / max(path_inradius, 1e-6), 0.0);
665        } else {
666            d = length(p) / path_boundary_radius(p, path_n);
667        }
668    } else if (coord_mode < 0.5) {
669        // Mode 0 — a band of the coordinate is a band of constant DISTANCE,
670        // which is the definition of an offset curve (ADR-0105). This is the
671        // default and it is bit-for-bit the arithmetic that shipped.
672        d = mark_distance(p, shape, points, star, rough);
673    } else {
674        // Mode 1 — a band of the coordinate is a band of constant SCALING, so
675        // its level sets are scaled copies of the outline (ADR-0111). On a
676        // polygon that keeps the corners the offsets round off; on a heart it
677        // keeps the notch, which is the construction the reference images are.
678        d = length(p) / max(mark_boundary_radius(p, shape, points, star, rough), 1e-6);
679    }
680
681    // **The stroke's screen width, taken before any branch.** A derivative has
682    // to be evaluated in uniform control flow, and hoisting it is what keeps
683    // that true however the branch below is compiled — `band_contour_ink` hoists
684    // its own for the same reason.
685    let d_width = max(fwidth(d), 1e-5);
686    // The response exponent, applied to the distance BEFORE it becomes a palette
687    // coordinate — so it reshapes where the contours sit rather than which
688    // colours they take. Above 1 the bands crowd toward the centre, which is what
689    // the reference images do and what a raw (evenly spaced) distance cannot.
690    // `select` rather than a branch, and the identity is exact: `pow(x, 1.0)` is
691    // not bit-exact, so an unbound preset must not go through it (ADR-0092).
692    let shaped = select(pow(d, gamma), d, gamma == 1.0);
693    let coord = shaped * color_span + color_center;
694
695    // Hard bands, then the contour drawn from the SAME coordinate (ADR-0078).
696    let banded = band_coord(coord, palette_steps);
697    let ca = textureSample(lut_a, lut_samp, vec2<f32>(banded, 0.5)).rgb;
698    let cb = textureSample(lut_b, lut_samp, vec2<f32>(banded, 0.5)).rgb;
699    var col = mix(ca, cb, clamp(palette_mix, 0.0, 1.0));
700    col = band_contour_ink(
701        col, coord, palette_steps, palette_contour, palette_contour_style,
702        palette_contour_ink, lut_a, lut_b, lut_samp, palette_mix
703    );
704    col = apply_saturation(col, saturation);
705
706    // **Fill and stroke are one field, not two routes** (ADR-0107). `d` is the
707    // single evaluation above; the interior is `d < 1` and the outline is
708    // `abs(d - 1) < w`, so a stroke cannot drift off the fill it belongs to
709    // because there is nothing for it to drift from. (The ADR writes the pair
710    // as `d < 0` and `abs(d) < w` against a raw signed distance; this scene's
711    // coordinate is that distance normalized to 1 on the outline, so the two
712    // tests are the same two tests shifted by one.)
713    //
714    // **`stroke` is a width in the coordinate, and the coordinate is a metric
715    // distance only at a WHOLE `shape`** (ADR-0226). Mid-travel `d` is a blend
716    // of two arms' fields, so `abs(d - 1) < stroke` still finds the blended
717    // outline — the two arms both read 1 there — but the band's thickness on
718    // screen is not the width a whole index would draw, and it varies around the
719    // figure. The same qualification covers the spacing of the bands above.
720    //
721    // Exactly 0 is the identity and takes the branch away, which is what keeps
722    // every shipped preset and every golden baseline on the arithmetic it has.
723    if (stroke > 0.0) {
724        col = col * (1.0 - smoothstep(stroke - d_width, stroke + d_width, abs(d - 1.0)));
725    }
726
727    // Alpha: this field covers every pixel, which is the coverage it honestly
728    // has (ADR-0056). `occlude` scales how much of that the backdrop underneath
729    // resolves against (ADR-0085). Reached only when no post stage is active;
730    // the chain owns the seam otherwise and the renderer hands a literal 1.0.
731    return vec4<f32>(col, params.d.x);
732}
733"#;
734
735#[repr(C)]
736#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
737struct Params {
738    a: [f32; 4],
739    b: [f32; 4],
740    c: [f32; 4],
741    d: [f32; 4],
742    e: [f32; 4],
743    f: [f32; 4],
744    /// x: palette_contour_style, y: palette_contour_ink (ADR-0197); zw unused.
745    g: [f32; 4],
746    /// The authored contour, two points per element — see the WGSL's `path_pt`.
747    /// Written every frame with the rest of the struct; it changes only on a
748    /// preset switch, and 1.5 KB of `write_buffer` is far below the cost of
749    /// splitting it into a second binding whose layout shape would then have to
750    /// be argued against ADR-0058.
751    path: [[f32; 4]; PATH_VEC4S],
752}
753
754/// A fullscreen signed-distance figure from the shared mark roster, coloured
755/// through the shared palette.
756pub struct ShapeFieldScene {
757    /// The pipeline, the uniform buffer, the 256x1 gradient LUT pair (A/B) the
758    /// fragment samples + crossfades for colour (ADR-0021), and the one bind
759    /// group this scene binds.
760    gpu: gpu::FullscreenScene,
761    /// The silhouette and its point count, raw as the preset bound them —
762    /// `marks::mark_shape` / `mark_points` condition them on the way to the
763    /// uniform, which is where a selector's precondition belongs (the
764    /// `kaleido_edge` precedent). Only `mark_points` quantizes; `mark_shape`
765    /// clamps and leaves the fraction, which is what lets a bound `shape`
766    /// travel between two arms (ADR-0226).
767    shape: f32,
768    points: f32,
769    /// The `star` arm's shape params and its hand-drawn controls, raw as the
770    /// preset bound them (Plan 0091 Phase 5). `marks::star_*` condition them on
771    /// the way to the uniform. Inert on every other silhouette, and nothing
772    /// warns — `presets/README.md` carries that.
773    star_valley: f32,
774    star_curve: f32,
775    star_jitter: f32,
776    star_seed: f32,
777    star_wobble: f32,
778    star_wobble_freq: f32,
779    scale: f32,
780    /// The shared palette knobs (ADR-0021). This scene has no `hue` or
781    /// `brightness`.
782    colour: common::PaletteParams,
783    /// The shared view transform (ADR-0018).
784    pan: common::PanParams,
785    color_span: f32,
786    color_center: f32,
787    /// The response exponent on the distance, raw as the preset bound it;
788    /// [`applied_gamma`] conditions it on the way to the uniform.
789    gamma: f32,
790    /// Which coordinate the palette is handed, raw as the preset bound it;
791    /// [`applied_coord_mode`] quantizes it on the way to the uniform, which is
792    /// where a selector's precondition belongs.
793    coord_mode: f32,
794    /// The figure's own turn, in radians, raw as the preset bound it. Applied
795    /// about the figure's centre rather than the frame's — see the shader.
796    rotation: f32,
797    /// The stroke half-width in coordinate units, raw as the preset bound it;
798    /// [`applied_stroke`] conditions it on the way to the uniform. `0` — the
799    /// default — is the filled figure and an exact arithmetic identity.
800    stroke: f32,
801    /// The authored contour, and the one `morph` travels towards (ADR-0107).
802    /// Both are set by [`Scene::configure`] on a preset switch and by nothing
803    /// else — geometry is structural, not a param, so `reset_params` does not
804    /// touch them.
805    ///
806    /// Fewer than 3 points in `path_from` means no authored contour and the
807    /// scene draws the `marks` roster, which is what every preset declaring no
808    /// `[path]` gets. An empty `path_to` means no morph target, so `morph` is
809    /// inert and the packed contour is `path_from` verbatim.
810    path_from: Vec<[f32; 2]>,
811    path_to: Vec<[f32; 2]>,
812    /// The same authored outline as a **G1-continuous chain of circular arcs**,
813    /// fitted at load through the line renderer's own fitter (ADR-0098). Empty
814    /// where the fit was not worth keeping, and unread while a morph is in
815    /// flight or under the scaled-copy coordinate — see [`Self::pack_path`].
816    pieces: Vec<crate::render::scenes::lines::biarc::Piece>,
817    /// The contour packed for the uniform — the interpolation of the two above
818    /// at this frame's `morph`, rebuilt in `render`.
819    ///
820    /// A field rather than a local so the per-frame pack writes into an
821    /// allocation made once. This is the render thread, not the audio callback,
822    /// but the rule that a per-frame path does not allocate is the same one.
823    path: Box<[[f32; 4]; PATH_VEC4S]>,
824    /// The two contours' inradii — the distance from each one's deepest interior
825    /// point to its own outline, measured by [`contour_inradius`] at configure
826    /// time.
827    ///
828    /// It is the divisor that makes the authored figure's coordinate `0` at that
829    /// deepest point and `1` on the outline, which is the contract every
830    /// silhouette in this scene meets (`marks`' own header states it for the
831    /// roster).
832    ///
833    /// **Mid-morph the two are interpolated rather than re-measured.** The true
834    /// inradius of an interpolated contour is not the interpolation of the two
835    /// inradii, and measuring it is a grid search — load-time work, not
836    /// per-frame. The error is bounded and one-sided in the direction that
837    /// matters: the shader clamps the coordinate at 0 from below, so an
838    /// underestimate costs nothing and an overestimate leaves the innermost
839    /// sliver short of the palette's first texel.
840    path_inradius: f32,
841    path_inradius_to: f32,
842    /// Whether the contour(s) this scene is holding are star-shaped about the
843    /// centre `coord_mode = 1` divides by — `None` where the preset declares no
844    /// `[path]` and the roster arm is the figure.
845    ///
846    /// Read off the parsed shape at configure time, where the geometry is
847    /// (`PathShape::star_shaped`), and `false` when EITHER endpoint of a morph
848    /// pair fails: both are drawn. **An intermediate contour of a morph is not
849    /// tested** — it exists only between two frames' interpolation, and two
850    /// star-shaped endpoints can pass through a contour that is not.
851    path_star_shaped: Option<bool>,
852    /// How far along `path_from` -> `path_to` the figure is, raw as the preset
853    /// bound it. `0` — the default — is the authored figure, and an exact
854    /// identity: the interpolation is skipped entirely.
855    morph: f32,
856    /// How much of this field's (total) coverage the backdrop resolves against
857    /// (ADR-0085). Set by the renderer every frame — **not** a named param, so
858    /// it is not reset by `reset_params`.
859    occlude: f32,
860}
861
862impl ShapeFieldScene {
863    /// Build the scene's pipeline and uniform buffer on `device`.
864    pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
865        // The mark roster's chunk is prepended, exactly as the two particle
866        // scenes do it: it declares no bindings and no entry points, so
867        // splicing it in changes nothing about this pipeline's layout.
868        let source = format!("{}{SHADER}", marks::sdf_wgsl());
869        let shader = gpu::fullscreen_shader(
870            device,
871            "shape-field-shader",
872            gpu::FULLSCREEN_VS_NDC,
873            &source,
874        );
875        let parts = gpu::FullscreenParts::new(device, "shape-field", std::mem::size_of::<Params>());
876        // One group, sampler first and uniform last — see the WGSL's note for
877        // why this shape and not `fragment_field`'s two-group split. The uniform
878        // entry is a full literal rather than `gpu::uniform` because that helper
879        // passes `min_binding_size: None`, and declaring one is half of what
880        // makes this shape unique.
881        let bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
882            label: Some("shape-field-bind-layout"),
883            entries: &[
884                gpu::sampler(0),
885                gpu::texture(1, true),
886                gpu::texture(2, true),
887                wgpu::BindGroupLayoutEntry {
888                    binding: 3,
889                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
890                    ty: wgpu::BindingType::Buffer {
891                        ty: wgpu::BufferBindingType::Uniform,
892                        has_dynamic_offset: false,
893                        min_binding_size: wgpu::BufferSize::new(
894                            std::mem::size_of::<Params>() as u64
895                        ),
896                    },
897                    count: None,
898                },
899            ],
900        });
901        // This layout binds the sampler first and the two textures after it, so
902        // the pair's role-ordered array is destructured into binding order here.
903        let [lut_a, lut_b, lut_sampler] = parts.luts().bind_entries(1, 2, 0);
904        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
905            label: Some("shape-field-bind-group"),
906            layout: &bind_layout,
907            entries: &[
908                lut_sampler,
909                lut_a,
910                lut_b,
911                wgpu::BindGroupEntry {
912                    binding: 3,
913                    resource: parts.uniforms().as_entire_binding(),
914                },
915            ],
916        });
917
918        Self {
919            gpu: parts.finish(
920                device,
921                &shader,
922                &[&bind_layout],
923                bind_group,
924                None,
925                surface_format,
926                wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
927                "shape-field",
928            ),
929            shape: marks::DEFAULT_SHAPE,
930            points: marks::DEFAULT_POINTS,
931            star_valley: marks::DEFAULT_STAR_VALLEY,
932            star_curve: marks::DEFAULT_STAR_CURVE,
933            star_jitter: marks::DEFAULT_STAR_JITTER,
934            star_seed: marks::DEFAULT_STAR_SEED,
935            star_wobble: marks::DEFAULT_STAR_WOBBLE,
936            star_wobble_freq: marks::DEFAULT_STAR_WOBBLE_FREQ,
937            scale: DEFAULT_SCALE,
938            colour: common::PaletteParams::new(0.0, common::DEFAULT_BRIGHTNESS),
939            pan: common::PanParams::default(),
940            color_span: DEFAULT_COLOR_SPAN,
941            color_center: DEFAULT_COLOR_CENTER,
942            gamma: DEFAULT_GAMMA,
943            coord_mode: DEFAULT_COORD_MODE,
944            rotation: DEFAULT_ROTATION,
945            stroke: DEFAULT_STROKE,
946            morph: DEFAULT_MORPH,
947            path_from: Vec::new(),
948            path_to: Vec::new(),
949            pieces: Vec::new(),
950            path: Box::new([[0.0; 4]; PATH_VEC4S]),
951            path_inradius: 1.0,
952            path_inradius_to: 1.0,
953            path_star_shaped: None,
954            occlude: crate::render::post::DEFAULT_OCCLUDE,
955        }
956    }
957}
958
959/// The `scale` the shader is handed: held inside the range the arithmetic needs,
960/// with a non-finite binding falling back to the default.
961///
962/// The clamp is CPU-side for the reason `background::applied_ramp_gamma` states:
963/// it can never be reached with a NaN, where WGSL's `clamp` is
964/// implementation-defined, and the default stays **exactly** the default on the
965/// way to the uniform.
966fn applied_scale(scale: f32) -> f32 {
967    if scale.is_finite() {
968        scale.clamp(MIN_SCALE, MAX_SCALE)
969    } else {
970        DEFAULT_SCALE
971    }
972}
973
974/// The `rotation` the shader is handed: passed through, with a non-finite
975/// binding falling back to the identity.
976///
977/// No clamp, because an angle wraps and there is no end of the range to hold it
978/// inside — the same treatment `lines/star.rs` gives the name. The finiteness
979/// guard is not decoration: `cos(NaN)` is `NaN`, and one `NaN` in the figure's
980/// own frame takes every pixel of the frame with it.
981fn applied_rotation(rotation: f32) -> f32 {
982    if rotation.is_finite() {
983        rotation
984    } else {
985        DEFAULT_ROTATION
986    }
987}
988
989/// The `coord_mode` the shader is handed: clamped into the roster, then
990/// **rounded to an integer**, with a non-finite binding falling back to the
991/// default — and **forced back to the distance on a figure the scaled copy has
992/// no single value on**.
993///
994/// The quantizing half is the `kaleido_edge` precedent, and `marks::mark_points`
995/// beside it. A mode's values are **identities** rather than a quantity:
996/// `[smoothing]` and preset dissolves interpolate a binding continuously from
997/// one setting to another, so easing the distance to the radius passes through
998/// 0.4 and 0.6, and there is nothing halfway between an offset curve and a
999/// scaled copy for the shader to draw there. `marks::mark_shape` is the
1000/// contrasting case rather than the parallel one: a position between two
1001/// silhouettes is a figure (ADR-0226), and a position between two coordinate
1002/// modes is not.
1003///
1004/// # The fallback, and why it is not silent
1005///
1006/// The scaled copy divides by the boundary radius along a ray from the figure's
1007/// centre, so it needs a figure every such ray leaves exactly once. Two figures
1008/// this scene can draw are not: a **`ring`**, whose centre is in its hole, and
1009/// an **authored contour that is not star-shaped about its centre** — a
1010/// crescent, or a silhouette whose sinuses put one lobe across the ray into
1011/// another. On both the shader takes the outermost crossing and everything
1012/// between the crossings reads as interior, which collapses the figure to a dot
1013/// inside a few huge rays.
1014///
1015/// A roster position that only *touches* the `ring` — anywhere in the open
1016/// travel between `disc` and `polygon` (ADR-0226) — inherits the defect from the
1017/// side it blends, so `marks::shape_touches_ring` is the test rather than
1018/// equality. At a whole index the two are the same test.
1019///
1020/// `contour_star_shaped` is the verdict for the contour being drawn, or `None`
1021/// where there is no `[path]` table — in which case the roster arm is the figure
1022/// and the `ring` is the one arm that fails. A contour REPLACES the roster arm
1023/// in the shader, so the two tests are alternatives rather than both.
1024///
1025/// The `ring`'s case is the one behavioural choice ADR-0111 leaves open. Plan
1026/// 0098 Phase 4 rendered the three defensible answers before picking, and what
1027/// settled it is that defining the boundary as the outer rim produces a figure
1028/// **byte-identical to a `disc`**: the coordinate collapses to `length(p)` and
1029/// the hole stops existing, so a preset naming one roster entry would be shown
1030/// another. That is the negative ADR-0111 records, reached in practice.
1031///
1032/// So the combination is refused rather than approximated, and the refusal is
1033/// **announced**: `Preset::from_toml_str` warns at load when a preset rests on
1034/// it (ADR-0020's shape, the `thickness` dead-zone precedent), on the same
1035/// condition this decides on. The silent fallback was the third candidate and it
1036/// is the one this rejects — it renders the same pixels as this does and costs
1037/// an author the afternoon.
1038fn applied_coord_mode(mode: f32, shape: f32, contour_star_shaped: Option<bool>) -> f32 {
1039    let single_valued = match contour_star_shaped {
1040        Some(star_shaped) => star_shaped,
1041        None => !marks::shape_touches_ring(shape),
1042    };
1043    if !single_valued {
1044        return DEFAULT_COORD_MODE;
1045    }
1046    if mode.is_finite() {
1047        mode.clamp(MIN_COORD_MODE, MAX_COORD_MODE).round()
1048    } else {
1049        DEFAULT_COORD_MODE
1050    }
1051}
1052
1053/// The exponent the shader will **actually apply** for a bound `gamma`: a
1054/// non-finite binding falls back to the identity, and a finite one is held
1055/// inside the positive range ([`MIN_GAMMA`], [`MAX_GAMMA`]).
1056///
1057/// CPU-side for `ink::applied_gamma`'s two reasons: `1.0` stays **exactly**
1058/// `1.0` on the way to the uniform, which is what the shader's identity branch
1059/// tests, and the clamp can never be reached with a NaN, where WGSL's `clamp` is
1060/// implementation-defined.
1061fn applied_gamma(gamma: f32) -> f32 {
1062    if gamma.is_finite() {
1063        gamma.clamp(MIN_GAMMA, MAX_GAMMA)
1064    } else {
1065        DEFAULT_GAMMA
1066    }
1067}
1068
1069/// The `stroke` the shader is handed: held inside its range, with a non-finite
1070/// binding falling back to the filled figure.
1071///
1072/// **`0` survives as exactly `0`**, which the shader's identity branch tests
1073/// for: a filled figure must execute none of the stroke arithmetic, so every
1074/// shipped preset and every golden baseline stays on what it has (ADR-0092's
1075/// care, the same reason `gamma` and `rotation` have identity branches).
1076fn applied_stroke(stroke: f32) -> f32 {
1077    if stroke.is_finite() {
1078        stroke.clamp(0.0, MAX_STROKE)
1079    } else {
1080        DEFAULT_STROKE
1081    }
1082}
1083
1084/// The `morph` the contour is interpolated at: held inside `0..=1`, with a
1085/// non-finite binding falling back to the authored figure.
1086///
1087/// Clamped rather than wrapped, and not extrapolated past either end: outside
1088/// `0..=1` the interpolation leaves both authored silhouettes behind and the
1089/// figure is one nobody drew — which is a different thing from the mid-morph
1090/// shapes nobody drew, because those at least lie between two that someone did.
1091fn applied_morph(morph: f32) -> f32 {
1092    if morph.is_finite() {
1093        morph.clamp(0.0, 1.0)
1094    } else {
1095        DEFAULT_MORPH
1096    }
1097}
1098
1099/// The contour's **inradius**: the distance from its deepest interior point to
1100/// its own outline, in the normalized `[-1, 1]` frame the contour lives in.
1101///
1102/// Measured rather than derived, because a closed contour has no closed form for
1103/// it. A coarse grid over the box finds the deepest cell, then three rounds of
1104/// local search shrink around it — so the reading is the grid's resolution only
1105/// until the refinement, and the refinement halves its neighbourhood each round.
1106///
1107/// **It can still land a hair short**, which is why the shader clamps the
1108/// coordinate at 0 from below rather than trusting this. Short is the safe
1109/// direction: it makes the innermost sliver of the figure read as the palette's
1110/// first texel, where over-reporting would leave the interior never reaching it.
1111fn contour_inradius(points: &[[f32; 2]]) -> f32 {
1112    /// Cells per axis of the first pass, over the `[-1, 1]` box.
1113    const GRID: i32 = 96;
1114    /// Local refinement rounds, each halving the search radius.
1115    const REFINE: u32 = 12;
1116
1117    let depth_at = |p: [f32; 2]| -> f32 {
1118        // Unsigned distance to the closing polygon, and a crossing parity for
1119        // whether `p` is inside it — the CPU counterpart of the WGSL's
1120        // `path_sd`, kept to the one quantity the shader needs from the CPU
1121        // rather than mirroring the whole field.
1122        let n = points.len();
1123        let mut best = f32::INFINITY;
1124        let mut inside = false;
1125        for i in 0..n {
1126            let (Some(&a), Some(&b)) = (points.get(i), points.get((i + n - 1) % n)) else {
1127                continue;
1128            };
1129            let e = [b[0] - a[0], b[1] - a[1]];
1130            let w = [p[0] - a[0], p[1] - a[1]];
1131            let ee = (e[0] * e[0] + e[1] * e[1]).max(1e-20);
1132            let t = ((w[0] * e[0] + w[1] * e[1]) / ee).clamp(0.0, 1.0);
1133            let q = [w[0] - e[0] * t, w[1] - e[1] * t];
1134            best = best.min(q[0] * q[0] + q[1] * q[1]);
1135            let c1 = p[1] >= a[1];
1136            let c2 = p[1] < b[1];
1137            let c3 = e[0] * w[1] > e[1] * w[0];
1138            if (c1 && c2 && c3) || (!c1 && !c2 && !c3) {
1139                inside = !inside;
1140            }
1141        }
1142        if inside { best.sqrt() } else { 0.0 }
1143    };
1144
1145    let mut best_p = [0.0f32, 0.0];
1146    let mut best_d = depth_at(best_p);
1147    for gy in 0..=GRID {
1148        for gx in 0..=GRID {
1149            let p = [
1150                (gx as f32 / GRID as f32) * 2.0 - 1.0,
1151                (gy as f32 / GRID as f32) * 2.0 - 1.0,
1152            ];
1153            let d = depth_at(p);
1154            if d > best_d {
1155                best_d = d;
1156                best_p = p;
1157            }
1158        }
1159    }
1160    let mut radius = 2.0 / GRID as f32;
1161    for _ in 0..REFINE {
1162        for (dx, dy) in [
1163            (-1.0f32, 0.0f32),
1164            (1.0, 0.0),
1165            (0.0, -1.0),
1166            (0.0, 1.0),
1167            (-1.0, -1.0),
1168            (1.0, -1.0),
1169            (-1.0, 1.0),
1170            (1.0, 1.0),
1171        ] {
1172            let p = [best_p[0] + dx * radius, best_p[1] + dy * radius];
1173            let d = depth_at(p);
1174            if d > best_d {
1175                best_d = d;
1176                best_p = p;
1177            }
1178        }
1179        radius *= 0.5;
1180    }
1181    // A contour with no interior the search could find would divide the whole
1182    // frame by zero; the floor keeps the coordinate finite and the figure reads
1183    // as all exterior, which is what a zero-area contour is.
1184    best_d.max(1e-4)
1185}
1186
1187/// The palette coordinate this scene hands the LUT, as a CPU mirror of the
1188/// shader's two lines — so the exponent's properties are testable without a GPU
1189/// (the arrangement `ink::key` and `tonemap::map` both use).
1190#[cfg(test)]
1191pub(crate) fn coord(distance: f32, gamma: f32, color_span: f32, color_center: f32) -> f32 {
1192    let g = applied_gamma(gamma);
1193    let shaped = if g == 1.0 { distance } else { distance.powf(g) };
1194    shaped * color_span + color_center
1195}
1196
1197/// The parameter names this scene consumes — the vocabulary a preset binding is
1198/// checked against at load (ADR-0020). **Keep in sync with `set_param` below**;
1199/// `declared_params_match_set_param` in `core/tests/suite/preset.rs` fails if the two
1200/// drift.
1201pub const PARAMS: &[ParamSpec] = &[
1202    crate::render::scenes::marks::SHAPE,
1203    crate::render::scenes::marks::POINTS,
1204    crate::render::scenes::marks::STAR_VALLEY,
1205    crate::render::scenes::marks::STAR_CURVE,
1206    crate::render::scenes::marks::STAR_JITTER,
1207    crate::render::scenes::marks::STAR_SEED,
1208    crate::render::scenes::marks::STAR_WOBBLE,
1209    crate::render::scenes::marks::STAR_WOBBLE_FREQ,
1210    ParamSpec {
1211        name: "scale",
1212        default: 0.6,
1213        range: Some([0.05, 2.0]),
1214        doc: "Size of the shape within the frame.",
1215        kind: ParamKind::Modal,
1216        group: ParamGroup::Shape,
1217        main: true,
1218    },
1219    crate::render::scenes::common::PAN_X,
1220    crate::render::scenes::common::PAN_Y,
1221    ParamSpec {
1222        name: "color_span",
1223        default: 0.6,
1224        range: Some([0.0, 1.0]),
1225        doc: "How much of the palette the field's range covers.",
1226        kind: ParamKind::Modal,
1227        group: ParamGroup::Colour,
1228        main: false,
1229    },
1230    ParamSpec {
1231        name: "color_center",
1232        default: 0.0,
1233        range: Some([-1.0, 1.0]),
1234        doc: "Shifts which part of that range lands in the middle of the palette.",
1235        kind: ParamKind::Modal,
1236        group: ParamGroup::Colour,
1237        main: false,
1238    },
1239    crate::render::scenes::common::SATURATION,
1240    crate::render::scenes::common::PALETTE_MIX,
1241    crate::render::scenes::common::PALETTE_STEPS,
1242    crate::render::scenes::common::PALETTE_CONTOUR,
1243    crate::render::scenes::common::PALETTE_CONTOUR_STYLE,
1244    crate::render::scenes::common::PALETTE_CONTOUR_INK,
1245    ParamSpec {
1246        name: "gamma",
1247        default: 1.0,
1248        range: Some([0.25, 4.0]),
1249        doc: "Shapes the falloff from the shape's edge; below 1 it bites sooner.",
1250        kind: ParamKind::Modal,
1251        group: ParamGroup::Light,
1252        main: false,
1253    },
1254    ParamSpec {
1255        name: "coord_mode",
1256        default: 0.0,
1257        // The top of the range is the roster's last index, which is where
1258        // `applied_coord_mode` clamps. A range above it advertises a mode that
1259        // silently resolves to another one.
1260        range: Some([MIN_COORD_MODE, MAX_COORD_MODE]),
1261        doc: "Which coordinate frame the distance is measured in, which changes the shape's whole geometry.",
1262        kind: ParamKind::Structural,
1263        group: ParamGroup::Shape,
1264        main: false,
1265    },
1266    ParamSpec {
1267        name: "rotation",
1268        default: 0.0,
1269        range: Some([0.0, std::f32::consts::TAU]),
1270        doc: "Turns the shape, in radians.",
1271        kind: ParamKind::Modal,
1272        group: ParamGroup::Motion,
1273        main: true,
1274    },
1275    ParamSpec {
1276        name: "stroke",
1277        default: 0.0,
1278        range: Some([0.0, 1.0]),
1279        doc: "Draws the outline instead of the filled figure, at this half-width; 0 fills.",
1280        kind: ParamKind::Modal,
1281        group: ParamGroup::Shape,
1282        main: false,
1283    },
1284    ParamSpec {
1285        name: "morph",
1286        default: 0.0,
1287        range: Some([0.0, 1.0]),
1288        doc: "Travels the authored path towards its morph_to silhouette; inert without one.",
1289        kind: ParamKind::Modal,
1290        group: ParamGroup::Motion,
1291        main: false,
1292    },
1293];
1294
1295impl ShapeFieldScene {
1296    /// Pack this frame's contour into the uniform array and report `(point
1297    /// count, inradius)` for the uniform's scalars.
1298    ///
1299    /// **The morph is interpolated here, on the CPU, once per frame** — not per
1300    /// pixel in the shader. The alternative was to hand the GPU both contours
1301    /// and lerp inside the distance loop, which would double the uniform, double
1302    /// the per-pixel loads, and re-derive at 2 M pixels a value that changes once
1303    /// a frame. `morph` is a parameter, not geometry.
1304    ///
1305    /// At `morph = 0`, or with no target, the authored contour is copied
1306    /// verbatim and no interpolation runs — the identity every other param on
1307    /// this scene keeps.
1308    fn pack_path(&mut self, coord_mode: f32) -> (usize, usize, f32) {
1309        let n = self.path_from.len().min(MAX_SAMPLES);
1310        if n < 3 {
1311            return (0, 0, 1.0);
1312        }
1313        let morphing = self.path_to.len() == self.path_from.len();
1314
1315        // **The arc chain serves where it can, and the polyline everywhere
1316        // else.** Two things put a figure back on points, and both are the
1317        // chain's own limits rather than a preference:
1318        //
1319        // - **a morph in flight.** Phase-correspondent points interpolate; two
1320        //   arc chains have no such correspondence, and inventing one is the
1321        //   representation problem ADR-0075 exists about. So a morphing pair
1322        //   travels on the polyline it was aligned as.
1323        // - **the scaled-copy coordinate.** `coord_mode = 1` needs the boundary
1324        //   radius along a ray, which is a second intersection routine the chain
1325        //   does not carry.
1326        if !morphing && coord_mode < 0.5 && !self.pieces.is_empty() {
1327            let pieces = self.pieces.len().min(MAX_ARC_PIECES);
1328            self.pack_pieces(pieces);
1329            return (0, pieces, self.path_inradius);
1330        }
1331
1332        let t = if morphing {
1333            applied_morph(self.morph)
1334        } else {
1335            0.0
1336        };
1337        for i in 0..n {
1338            let Some(&a) = self.path_from.get(i) else {
1339                continue;
1340            };
1341            let p = match self.path_to.get(i) {
1342                Some(&b) if t != 0.0 => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t],
1343                _ => a,
1344            };
1345            let slot = i >> 1;
1346            let half = (i & 1) * 2;
1347            if let Some(v) = self.path.get_mut(slot) {
1348                if let Some(x) = v.get_mut(half) {
1349                    *x = p[0];
1350                }
1351                if let Some(y) = v.get_mut(half + 1) {
1352                    *y = p[1];
1353                }
1354            }
1355        }
1356        let inradius = self.path_inradius + (self.path_inradius_to - self.path_inradius) * t;
1357        (n, 0, inradius.max(1e-4))
1358    }
1359
1360    /// Pack the fitted arc chain into the uniform: four elements per piece, in
1361    /// the layout the WGSL's `Params.path` comment spells out.
1362    ///
1363    /// The sector test's mid direction and half-angle are computed **here**, so
1364    /// the fragment's own test is a dot product rather than an `atan2` per piece
1365    /// per pixel. Same for the endpoints, which the outside-the-sweep arm needs.
1366    fn pack_pieces(&mut self, count: usize) {
1367        use crate::render::scenes::lines::biarc::Piece;
1368        for i in 0..count {
1369            let Some(&piece) = self.pieces.get(i) else {
1370                continue;
1371            };
1372            let base = i * VEC4S_PER_PIECE;
1373            let (a, b) = (piece.start_point(), piece.end_point());
1374            let (head, sector, sweep) = match piece {
1375                Piece::Arc {
1376                    centre,
1377                    radius,
1378                    start,
1379                    sweep,
1380                } => {
1381                    let mid = start + sweep * 0.5;
1382                    (
1383                        [1.0, centre[0], centre[1], radius],
1384                        [mid.cos(), mid.sin(), (sweep.abs() * 0.5).cos(), 0.0],
1385                        [start, sweep, 0.0, 0.0],
1386                    )
1387                }
1388                Piece::Line { .. } => ([0.0; 4], [0.0; 4], [0.0; 4]),
1389            };
1390            for (offset, value) in [
1391                (0, head),
1392                (1, sector),
1393                (2, [a[0], a[1], b[0], b[1]]),
1394                (3, sweep),
1395            ] {
1396                if let Some(slot) = self.path.get_mut(base + offset) {
1397                    *slot = value;
1398                }
1399            }
1400        }
1401    }
1402}
1403
1404impl Scene for ShapeFieldScene {
1405    fn name(&self) -> &'static str {
1406        "shape field"
1407    }
1408
1409    fn set_occlude(&mut self, occlude: f32) {
1410        self.occlude = occlude;
1411    }
1412
1413    fn set_palette(&mut self, palette: &Palette) {
1414        self.gpu.set_palette(palette);
1415    }
1416
1417    fn reset_params(&mut self) {
1418        self.shape = marks::DEFAULT_SHAPE;
1419        self.points = marks::DEFAULT_POINTS;
1420        self.star_valley = marks::DEFAULT_STAR_VALLEY;
1421        self.star_curve = marks::DEFAULT_STAR_CURVE;
1422        self.star_jitter = marks::DEFAULT_STAR_JITTER;
1423        self.star_seed = marks::DEFAULT_STAR_SEED;
1424        self.star_wobble = marks::DEFAULT_STAR_WOBBLE;
1425        self.star_wobble_freq = marks::DEFAULT_STAR_WOBBLE_FREQ;
1426        self.scale = DEFAULT_SCALE;
1427        self.colour.reset();
1428        self.pan.reset();
1429        self.color_span = DEFAULT_COLOR_SPAN;
1430        self.color_center = DEFAULT_COLOR_CENTER;
1431        self.gamma = DEFAULT_GAMMA;
1432        self.coord_mode = DEFAULT_COORD_MODE;
1433        self.rotation = DEFAULT_ROTATION;
1434        self.stroke = DEFAULT_STROKE;
1435        self.morph = DEFAULT_MORPH;
1436    }
1437
1438    /// The `[path]` table (ADR-0107), which is the only structural config this
1439    /// scene takes.
1440    ///
1441    /// **Called on every `shape_field` preset switch, table or no table** — the
1442    /// loader hands `Some(Path { shape: None })` for a preset that declares
1443    /// none, exactly so this runs and clears the contour. Without that, a switch
1444    /// from a path preset to a roster one would keep drawing the outgoing
1445    /// preset's silhouette.
1446    fn configure(
1447        &mut self,
1448        cfg: &super::lines::GeneratorConfig,
1449    ) -> Option<super::lines::CapOverflow> {
1450        if let super::lines::GeneratorConfig::Path { shape, morph_to } = cfg {
1451            self.path_from.clear();
1452            self.path_to.clear();
1453            self.pieces.clear();
1454            self.path_inradius = 1.0;
1455            self.path_inradius_to = 1.0;
1456            self.path_star_shaped = None;
1457            if let Some(contour) = shape {
1458                // The load boundary already refused an arity above the ceiling,
1459                // so the `take` is a belt on a boundary that holds rather than a
1460                // decimation an author is not told about.
1461                self.path_from
1462                    .extend(contour.points().iter().take(MAX_SAMPLES).copied());
1463                self.path_inradius = contour_inradius(&self.path_from);
1464                self.path_star_shaped = Some(contour.star_shaped());
1465                self.pieces.extend_from_slice(contour.pieces());
1466            }
1467            // The pair is aligned at load; a target of a different arity would
1468            // mean the loader let one through, so it is dropped rather than
1469            // interpolated against the wrong correspondent.
1470            if let Some(target) = morph_to
1471                .as_ref()
1472                .filter(|t| t.points().len() == self.path_from.len())
1473            {
1474                self.path_to.extend(target.points().iter().copied());
1475                self.path_inradius_to = contour_inradius(&self.path_to);
1476                // Both endpoints are drawn, so either one failing takes the
1477                // scaled copy away for the whole travel.
1478                self.path_star_shaped =
1479                    Some(self.path_star_shaped.unwrap_or(true) && target.star_shaped());
1480            }
1481        }
1482        None
1483    }
1484
1485    fn set_param(&mut self, name: &str, value: f32) {
1486        // The shared param blocks first, this scene's own names after
1487        // (`scenes::common`).
1488        if self.colour.set(name, value) || self.pan.set(name, value) {
1489            return;
1490        }
1491        match name {
1492            "shape" => self.shape = value,
1493            "points" => self.points = value,
1494            "star_valley" => self.star_valley = value,
1495            "star_curve" => self.star_curve = value,
1496            "star_jitter" => self.star_jitter = value,
1497            "star_seed" => self.star_seed = value,
1498            "star_wobble" => self.star_wobble = value,
1499            "star_wobble_freq" => self.star_wobble_freq = value,
1500            "scale" => self.scale = value,
1501            "color_span" => self.color_span = value,
1502            "color_center" => self.color_center = value,
1503            "gamma" => self.gamma = value,
1504            "coord_mode" => self.coord_mode = value,
1505            "rotation" => self.rotation = value,
1506            "stroke" => self.stroke = value,
1507            "morph" => self.morph = value,
1508            _ => {}
1509        }
1510    }
1511
1512    fn update(&mut self, _frame: &AnalysisFrame) {
1513        // Fully parameter-driven; the analysis reaches this scene only through
1514        // the preset expressions bound to its parameters.
1515    }
1516
1517    fn render(
1518        &mut self,
1519        queue: &wgpu::Queue,
1520        encoder: &mut wgpu::CommandEncoder,
1521        view: &wgpu::TextureView,
1522        aspect: f32,
1523    ) {
1524        // Quantized once, because `applied_coord_mode` has to see the same value
1525        // the shader will: the `ring` refusal is a fact about the SELECTED arm,
1526        // not about the raw binding.
1527        let shape = marks::mark_shape(self.shape);
1528        self.gpu.flush_palette(queue);
1529        let coord_mode = applied_coord_mode(self.coord_mode, shape, self.path_star_shaped);
1530        let (path_count, path_arcs, path_inradius) = self.pack_path(coord_mode);
1531
1532        let params = Params {
1533            // `aspect` is the argument the chain hands down for the target this
1534            // scene is drawing into — never a size this scene chose (ADR-0037).
1535            a: [
1536                aspect.max(0.1),
1537                shape,
1538                marks::mark_points(self.points),
1539                applied_scale(self.scale),
1540            ],
1541            b: [self.pan.x, self.pan.y, self.color_span, self.color_center],
1542            c: [
1543                self.colour.saturation,
1544                self.colour.mix,
1545                palette::band_steps(self.colour.steps),
1546                palette::band_contour(self.colour.contour),
1547            ],
1548            d: [
1549                self.occlude,
1550                applied_gamma(self.gamma),
1551                coord_mode,
1552                applied_rotation(self.rotation),
1553            ],
1554            e: [
1555                marks::star_valley(self.star_valley),
1556                marks::star_curve(self.star_curve),
1557                marks::star_jitter(self.star_jitter),
1558                marks::star_seed(self.star_seed),
1559            ],
1560            f: [
1561                path_count as f32,
1562                path_inradius,
1563                applied_stroke(self.stroke),
1564                path_arcs as f32,
1565            ],
1566            g: [
1567                palette::band_contour_style(self.colour.contour_style),
1568                self.colour.contour_ink,
1569                marks::star_wobble(self.star_wobble),
1570                marks::star_wobble_freq(self.star_wobble_freq),
1571            ],
1572            path: *self.path,
1573        };
1574        self.gpu.write_uniform(queue, &params);
1575        self.gpu
1576            .draw(encoder, "shape-field-pass", view, wgpu::LoadOp::Load);
1577    }
1578}
1579
1580#[cfg(test)]
1581mod tests;