Skip to content

RectSL.kt

src/jvmMain/kotlin/afield/lang/rectsl/RectSL.kt · 5421 lines

package and imports — 45 lines, from line 1
package afield.lang.rectsl

import afield.lang.stacksl.StackSLCompileOptions
import afield.lang.stacksl.StackSLCompileResult
import afield.lang.stacksl.StackSLCompiler
import afield.lang.stacksl.StackSLSeverity
import afield.graphics.vulkan.Renderer
import afield.graphics.render.Module
import afield.graphics.render.Mesh
import afield.document.context.fromContext
import afield.graphics.render.parameters
import afield.graphics.render.pointer
import afield.graphics.vulkan.targets.RenderTarget
import afield.graphics.vulkan.targets.RenderToTexture
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import afield.document.graph.Box
import afield.graphics.color.Color4
import afield.graphics.color.Colormaps
import afield.runtime.kotlin.KotlinScriptEngine
import afield.math.functions.Functions
import afield.math.linalg.Matrix4f
import kotlin.script.experimental.api.ResultValue
import org.lwjgl.vulkan.VK10.VK_FORMAT_B8G8R8A8_SRGB
import java.nio.file.Files
import java.nio.file.Path
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.ceil
import kotlin.math.cos
import kotlin.math.floor
import kotlin.math.hypot
import kotlin.math.log10
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.round
import kotlin.math.sin
import kotlin.math.sqrt

RectSL — Field's rectangle scene language, and the largest single file in the tree.

A RectSL program is a finite animated scene made of rectangles, written as a small stack language inside a Kotlin triple-quoted string whose first line is #rectsl. It is not a simulation and not a renderer: the language decides where rectangles are over a fixed duration and what material metadata each one carries, and StackSL (afield.lang.stacksl) decides how a material shades. That division is the reason both are small.

The pipeline, in reading order

Four types, and every entry point in this file is a step between two of them:

String  --tokenize-->  List<Token>  --eval-->  SceneProgram  --sample(t)-->  SampledScene  --lower-->  RectMesh
  • RectLangTokenizer is the whole of the syntax. There is no parser: the language is postfix, so tokenizing is parsing.
  • RectLangVm is the evaluator, and where the vocabulary lives. Words spelled ->something are mutators that write through to the active rect, group or scene; everything else transforms stack values. The arrow is part of the word.
  • SceneProgram is the compiled result and the value worth understanding first. It is time-independent — a wall, a duration, an fps and a list of nodes whose properties are Tracks rather than numbers — which is what makes a scene something you can ask questions of rather than only play. sample(t) walks it and returns a SampledScene, a flat z-ordered list of SampledRects with no tracks left in it.
  • lowerSampledScene turns that into a RectMesh: four RectVertexes and six indices per rect, with colour, material index and per-object shader data interleaved, ready for a Vulkan buffer.

RectAnimationOracle is the one object that names all five steps, and compileRectSL is the same compile with host-built curves bound in by name — the afield.math.functions.Functions half of the bridge, so a program can lay out an array from a curve the host fitted or evolved.

Why a property is a track and not a number

Half this file is Track and its implementations, because a property being a function of time rather than a number is what the rest of the package is built on. A track is symbolic and composable — TcbTrack for Kochanek–Bartels keys, EasedScalarTrack, TimeWarpTrack, GateTrack for life windows, HeadingTrack for face-travel, plus unary/binary/ternary combinators — so a scene can be differentiated, fitted (RectSLFit.kt), measured (RectSLCoverage.kt computes exact pixel coverage in closed form) and translated to GLSL (RectSLGlsl.kt) rather than only evaluated frame by frame.

Where the rest of the package sits

RectSLPattern.kt / RectSLPatternOps.kt make a Tidal pattern the scene's structure — a pattern is a track and an event is a live window. RectSLSoftRaster.kt is the differentiable rasterizer, RectSLFn.kt lowers scenes into the Functions algebra, RectSLSdfLook.kt and RectSLGlsl.kt are the two GPU paths, RectSLSimplify.kt shrinks a program, and RectSLEditorExtension.kt is the three lines that register the browser-side editor mode.

RectSL cheatsheet is the word-by-word reference with worked pictures; RectSL examples is the gallery.

data class Vec2d(val x: Double, val y: Double)

operator fun plus(other: Vec2d): Vec2d = Vec2d(x + other.x, y + other.y)

operator fun minus(other: Vec2d): Vec2d = Vec2d(x - other.x, y - other.y)

operator fun times(scale: Double): Vec2d = Vec2d(x * scale, y * scale)

infix fun dot(other: Vec2d): Double = x * other.x + y * other.y

fun length(): Double = sqrt(this dot this)

fun normalized(): Vec2d

    fun normalized(): Vec2d {
        val l = length()
        return if (l <= 1e-12) Vec2d(1.0, 0.0) else Vec2d(x / l, y / l)
    }
}

data class Vec4f

data class Vec4f(
    val x: Float,
    val y: Float,
    val z: Float,
    val w: Float
) {

companion object

val ZERO = Vec4f(0f, 0f, 0f, 0f)

fun Color4.toVec4f(): Vec4f = Vec4f(r.toFloat(), g.toFloat(), b.toFloat(), a.toFloat())

Bridge a Color4 to RectSL's local Vec4f. Color4 and Colormaps live in package afield.graphics.color, so general colormap and visualization code does not depend on this microlanguage; this extension keeps the RGBA→Vec4f conversion where its only caller, the renderer path, lives.

internal fun normalizeValues(values: List<Double>): List<Double>

Map a numeric list to [0,1] by its own min/max; a flat list maps to all 0.5.

internal fun normalizeValues(values: List<Double>): List<Double> {
    if (values.isEmpty()) return values
    val lo = values.minOrNull()!!
    val hi = values.maxOrNull()!!
    val range = hi - lo
    return if (abs(range) <= 1e-12) values.map { 0.5 } else values.map { (it - lo) / range }
}

sealed interface Token

data class Word(val name: String) : Token

data class Number(val value: Double) : Token

data class StringLit(val value: String) : Token

data class ColorLit(val color: Color4) : Token

data class Quote(val tokens: List<Token>, val source: String) : Token

data class KotlinEscape(val source: String) : Token

{{ kotlin }} — a Kotlin expression evaluated in a with(Functions){…} scope.

class RectLangException(message: String) : IllegalArgumentException(message)

class RectEscapeContext

The array/loop context a {{ … }} Kotlin escape sees, so escaped Kotlin can vary per copy: i (index), n (count), f (fraction 0..1), f0 (cyclic), fc (centered -1..1), ic (centered index). All zero (n=1) outside a loop.

class RectEscapeContext(
    @JvmField val i: Double,
    @JvmField val n: Double,
    @JvmField val f: Double,
    @JvmField val f0: Double,
    @JvmField val fc: Double,
    @JvmField val ic: Double
)

sealed interface Value

data class VNum(val x: Double) : Value

data class VStr(val x: String) : Value

data class VColor(val rgba: Color4) : Value

data class VVec2(val x: Double, val y: Double) : Value

data class VPaint(val paint: PaintSpec) : Value

data class VTrack<T>(val track: Track<T>) : Value

data class VRect(val id: String) : Value

data class VGroup(val id: String) : Value

data class VSelection(val refs: List<NodeRef>) : Value

data class VQuote(val tokens: List<Token>, val source: String) : Value

data class VMaterialRef(val name: String) : Value

data class VFn(val fn: afield.math.functions.Functions.Fn) : Value

data class VData(val values: List<Double>) : Value

private fun Value.describe(): String = when (this) {
    is VNum -> "Number"
    is VStr -> "String"
    is VColor -> "Color"
    is VVec2 -> "Vec2"
    is VData -> "Data"
    is VPaint -> "Paint"
    is VTrack<*> -> "Track<${track.kind}>"
    is VRect -> "Rect"
    is VGroup -> "Group"
    is VSelection -> "Selection"
    is VQuote -> "Quote"
    is VMaterialRef -> "Material"
    is VFn -> "Fn"
    is VPat -> "Pattern"
    is VSig -> "Signal"
}

class RectLangTokenizer(private val source: String)

class RectLangTokenizer(private val source: String) {
    private var i = 0

val offsets: java.util.IdentityHashMap<Token, Int> = java.util.IdentityHashMap()

Where each token began in the source, by identity. Tokens are data classes — two 1s are equal — so only identity tells them apart; compileRectSLWithSource reads this to put a problem, and each rectangle, on the line it came from.

var failureOffset: Int?

Where the tokenizer gave up, when tokenize threw: the opening of an unclosed quote, string or escape, or the unexpected character.

    var failureOffset: Int? = null
        private set

    private companion object {
        // Longest match first so multi-char operators aren't split by their prefixes.
        private val OPERATORS = listOf("<=", ">=", "!=", "<>", "<", ">", "=")
    }

fun tokenize(): List<Token>

body — 179 lines, from line 223
    fun tokenize(): List<Token> {
        val tokens = mutableListOf<Token>()
        while (true) {
            skipWhitespaceAndComments()
            if (i >= source.length) break
            tokens += located {
                if (source.startsWith("'(", i)) parseQuote()
                else if (source.startsWith("{{", i)) parseKotlinEscape()
                else parseSingleToken()
            }
        }
        return tokens
    }

    private inline fun located(parse: () -> Token): Token {
        val start = i
        return parse().also { offsets[it] = start }
    }

    private fun parseKotlinEscape(): Token.KotlinEscape {
        val openedAt = i
        i += 2 // consume "{{"
        val start = i
        var depth = 0
        while (i < source.length) {
            if (depth == 0 && source.startsWith("}}", i)) {
                val raw = source.substring(start, i)
                i += 2
                return Token.KotlinEscape(raw.trim())
            }
            when (source[i]) {
                '{' -> depth++
                '}' -> depth--
            }
            i++
        }
        failureOffset = openedAt
        throw RectLangException("Unterminated {{ … }} Kotlin escape")
    }

    private fun parseQuote(): Token.Quote {
        val openedAt = i
        val contentStart = i + 2
        i += 2
        val tokens = mutableListOf<Token>()
        while (true) {
            skipWhitespaceAndComments()
            if (i >= source.length) {
                if (failureOffset == null) failureOffset = openedAt
                throw RectLangException("Unterminated quote literal: this '( has no closing )")
            }
            if (source[i] == ')') {
                val raw = source.substring(contentStart, i)
                i++
                return Token.Quote(tokens, raw.trim())
            }
            tokens += located {
                if (source.startsWith("'(", i)) parseQuote()
                else if (source.startsWith("{{", i)) parseKotlinEscape()
                else parseSingleToken(inQuote = true)
            }
        }
    }

    private fun parseSingleToken(inQuote: Boolean = false): Token {
        val ch = source[i]
        if (ch == '"') return parseString()
        if (ch == ':' || ch == ';') {
            i++
            return Token.Word(ch.toString())
        }
        if (ch == ')' && inQuote) { failureOffset = i; throw RectLangException("Unexpected ')' in quote") }

        // Comparison operators and the track-pair separator `=` are punctuation.
        // Recognise them explicitly (longest match first) so `0=0.2` splits into
        // `0` `=` `0.2` while `<=`, `>=`, `!=`, `<>` stay whole. `=` is also a
        // break character below, so it separates operands even when glued to them.
        matchOperator()?.let { return it }

        val start = i
        while (i < source.length) {
            val c = source[i]
            if (c.isWhitespace() || c == '#' || c == '"' || c == ';' || c == ':' || c == '=') break
            if (inQuote && c == ')') break
            if (source.startsWith("'(", i)) break
            i++
        }
        if (start == i) { failureOffset = i; throw RectLangException("Unexpected character '${source[i]}'") }
        val raw = source.substring(start, i)
        return atom(raw)
    }

    private fun matchOperator(): Token.Word? {
        for (op in OPERATORS) {
            if (source.startsWith(op, i)) {
                i += op.length
                return Token.Word(op)
            }
        }
        return null
    }

    private fun parseString(): Token.StringLit {
        val start = i
        i++
        val text = buildString {
            while (i < source.length) {
                val c = source[i++]
                if (c == '"') return Token.StringLit(toString())
                if (c == '\\' && i < source.length) {
                    val escaped = source[i++]
                    append(
                        when (escaped) {
                            'n' -> '\n'
                            'r' -> '\r'
                            't' -> '\t'
                            '"' -> '"'
                            '\\' -> '\\'
                            else -> escaped
                        }
                    )
                } else {
                    append(c)
                }
            }
        }
        failureOffset = start
        throw RectLangException(unterminatedStringMessage(start))
    }

    private fun unterminatedStringMessage(start: Int): String {
        val (line, column) = locationAt(start)
        val trailingQuoteHint = if (source.substring(start).trim() == "\"") {
            " This looks like a single trailing quote; if this source came from a Kotlin raw string, " +
                "close it with exactly three quotes (`\"\"\"`), not four (`\"\"\"\"`)."
        } else {
            ""
        }
        return "Unterminated string literal at line $line, column $column.$trailingQuoteHint"
    }

    private fun locationAt(index: Int): Pair<Int, Int> {
        var line = 1
        var column = 1
        for (n in 0 until index.coerceIn(0, source.length)) {
            if (source[n] == '\n') {
                line++
                column = 1
            } else {
                column++
            }
        }
        return line to column
    }

    private fun atom(raw: String): Token {
        val numberish = raw.trimEnd(',')
        val lower = numberish.lowercase()
        if (lower.startsWith("0x") && lower.length in 8..10) {
            val hex = lower.drop(2).toLongOrNull(16)
            if (hex != null) return Token.ColorLit(Color4.fromHex(hex))
        }
        numberish.toDoubleOrNull()?.let { return Token.Number(it) }
        return Token.Word(raw)
    }

    private fun skipWhitespaceAndComments() {
        while (i < source.length) {
            val c = source[i]
            when {
                c.isWhitespace() -> i++
                c == '#' -> {
                    while (i < source.length && source[i] != '\n') i++
                }
                else -> return
            }
        }
    }
}

enum class TrackValueKind

enum class TrackValueKind {
    Double,
    Paint
}

internal fun Double.glslFloat(): String

A Double as a GLSL float literal: always with a decimal point, so 2 does not silently become an int and drag an expression into integer arithmetic. Non-finite values cannot be spelled in GLSL at all, so they are pinned to the float range rather than emitted as something that will not compile.

internal fun Double.glslFloat(): String = when {
    isNaN() -> "0.0"
    this == Double.POSITIVE_INFINITY -> "3.402823466e38"
    this == Double.NEGATIVE_INFINITY -> "-3.402823466e38"
    else -> {
        val text = toString()
        if (text.contains('.') || text.contains('e') || text.contains('E')) text else "$text.0"
    }
}

enum class TrackOp1

The closed vocabulary of scalar operations that can appear inside a track.

Tracks used to carry bare (Double) -> Double closures. That is perfectly good on the JVM and completely opaque to everything else: you cannot print a closure, compare two of them, rewrite one, or emit it as GLSL. Naming the ops keeps the CPU behaviour identical while turning the track IR into a symbolic graph, so each op owns both halves — how it evaluates, and how it spells itself in a shader.

The GLSL is written to match the Kotlin exactly rather than to be idiomatic: the divide-by-zero guards are reproduced, and MOD spells out Kotlin's truncated % instead of reaching for GLSL's mod(), which floors and so disagrees for negative operands.

The third column, fn:, is the differentiable one — the op as a transform of afield.math.functions.Functions.Fn graphs, so a track can be handed to a gradient fitter. Not every op has one, and the ones that do not say so in the type rather than answering with something plausible: see RectSLFnLowering and the refusal catalogue in RectSLFn.kt.

enum class TrackOp1(
    val eval: (Double) -> Double,
    val glsl: (String) -> String,
    val fn: RectSLFnLowering<TrackFnUnary>
) {
    NEG({ -it }, { "-($it)" }, RectSLFnLowering.Lowered({ a -> with(Functions) { a.flip() } })),
    ABS({ abs(it) }, { "abs($it)" }, RectSLFnLowering.Lowered({ a -> with(Functions) { a.abs() } })),
    FLOOR(
        { floor(it) },
        { "floor($it)" },
        RectSLFnLowering.Lowered({ a -> with(Functions) { a.floor() } })
    ),

ceil(x) is -floor(-x), which IEEE makes exact rather than approximate: both negations are exact operations, so the round trip cannot lose a bit.

    CEIL(
        { ceil(it) },
        { "ceil($it)" },
        RectSLFnLowering.Lowered({ a -> with(Functions) { a.flip().floor().flip() } })
    ),
    FRACT(
        { it - floor(it) },
        { "fract($it)" },
        RectSLFnLowering.Lowered({ a -> with(Functions) { a - a.floor() } })
    ),
    SQRT(
        { sqrt(max(it, 0.0)) },
        { "sqrt(max($it, 0.0))" },
        // `max(x, 0)` is a clamp with an effectively infinite ceiling and `sqrt` is `pow(0.5)`; Fn
        // has neither op under its own name. Math.pow(x, 0.5) agrees with Math.sqrt(x) to an ulp.
        RectSLFnLowering.Lowered({ a ->
            with(Functions) { a.clamp(fnLeaf(0.0), fnLeaf(Double.MAX_VALUE)).pow(fnLeaf(0.5)) }
        })
    ),
    SIN({ sin(it) }, { "sin($it)" }, RectSLFnLowering.Lowered({ a -> fnSinOf(a) })),
    COS({ cos(it) }, { "cos($it)" }, RectSLFnLowering.Lowered({ a -> fnCosOf(a) })),
    SATURATE(
        { it.coerceIn(0.0, 1.0) },
        { "clamp($it, 0.0, 1.0)" },
        RectSLFnLowering.Lowered({ a -> fnClamp01(a) })
    )
}

enum class TrackOp2

declaration — 59 lines, from line 487
enum class TrackOp2(
    val eval: (Double, Double) -> Double,
    val glsl: (String, String) -> String,
    val fn: RectSLFnLowering<TrackFnBinary>
) {
    ADD(
        { a, b -> a + b },
        { a, b -> "($a + $b)" },
        RectSLFnLowering.Lowered({ a, b -> with(Functions) { a + b } })
    ),
    SUB(
        { a, b -> a - b },
        { a, b -> "($a - $b)" },
        RectSLFnLowering.Lowered({ a, b -> with(Functions) { a - b } })
    ),
    MUL(
        { a, b -> a * b },
        { a, b -> "($a * $b)" },
        RectSLFnLowering.Lowered({ a, b -> with(Functions) { a * b } })
    ),
    DIV(
        { a, b -> if (abs(b) <= 1e-12) 0.0 else a / b },
        { a, b -> "(abs($b) <= 1e-12 ? 0.0 : $a / $b)" },
        RectSLFnLowering.Lowered({ a, b -> fnGuardedQuotient(a, b) })
    ),
    MOD(
        { a, b -> if (abs(b) <= 1e-12) 0.0 else a % b },
        { a, b -> "(abs($b) <= 1e-12 ? 0.0 : $a - $b * trunc($a / $b))" },
        RectSLFnLowering.Refused(
            "Kotlin's `%` on doubles is the EXACT remainder — the JLS computes `a - b*q` with an " +
                "exactly-determined integer q and no rounding — and `a - b*trunc(a/b)` in double " +
                "arithmetic is not the same function once the quotient outgrows 53 bits. A " +
                "statically constant pair still folds",
            closable = true,
            missing = "an exact-remainder FnExpr, not a sign primitive. The sign is no longer the " +
                "obstacle: `trunc(z)` is `s*floor(z) - (1-s)*floor(-z)` with s = fnStepFrom(z), " +
                "which is exact at every double including both zeros. What defeats the " +
                "reconstruction is the rounding in the division, and it fails *silently as a zero*, " +
                "which is indistinguishable from the near-zero guard having fired. Measured on " +
                "2026-08-19: 1e17 % 3 is 1.0 and the reconstruction says 0.0; 1e15 % 0.1 is " +
                "0.04448884876874218 and it says 0.0; it is exact for every quotient below ~2^53"
        )
    ),
    POW(
        { a, b -> a.pow(b) },
        { a, b -> "pow($a, $b)" },
        RectSLFnLowering.Lowered({ a, b -> with(Functions) { a.pow(b) } })
    ),
    MIN(
        { a, b -> min(a, b) },
        { a, b -> "min($a, $b)" },
        RectSLFnLowering.Lowered({ a, b -> fnMinOf(a, b) })
    ),
    MAX(
        { a, b -> max(a, b) },
        { a, b -> "max($a, $b)" },
        RectSLFnLowering.Lowered({ a, b -> fnMaxOf(a, b) })
    )
}

enum class TrackOp3

enum class TrackOp3(
    val eval: (Double, Double, Double) -> Double,
    val glsl: (String, String, String) -> String,
    val fn: RectSLFnLowering<TrackFnTernary>
) {

Order-insensitive bounds, matching coerceIn(min(lo, hi), max(lo, hi)).

    CLAMP(
        { x, lo, hi -> x.coerceIn(min(lo, hi), max(lo, hi)) },
        { x, lo, hi -> "clamp($x, min($lo, $hi), max($lo, $hi))" },
        RectSLFnLowering.Lowered({ x, lo, hi ->
            with(Functions) { x.clamp(fnMinOf(lo, hi), fnMaxOf(lo, hi)) }
        })
    ),
    MIX(
        { a, b, t -> a + (b - a) * t },
        { a, b, t -> "mix($a, $b, $t)" },
        RectSLFnLowering.Lowered({ a, b, t -> with(Functions) { a + (b - a) * t } })
    ),
    SMOOTHSTEP(
        { low, high, x ->
            val denom = high - low
            val u = if (abs(denom) <= 1e-12) 0.0 else ((x - low) / denom).coerceIn(0.0, 1.0)
            u * u * (3.0 - 2.0 * u)
        },
        { low, high, x -> "(abs($high - $low) <= 1e-12 ? 0.0 : smoothstep($low, $high, $x))" },
        RectSLFnLowering.Lowered({ low, high, x -> fnGuardedSmoothstep(low, high, x) })
    )
}

enum class TimeWarpKind

How a TimeWarpTrack rewrites the time it hands to its source. Each kind takes one scalar parameter, fixed when the warp is built, so the whole warp is (kind, amount) rather than a closure over that amount.

The fn: column is a lookup transform rather than a value one, which Fn spells shift, scaleX and warp. All five lower. The two wrapping ones did not until afield.math.functions.Functions.Fn.floor existed, which is what it was added for — a folded clock is x - p·floor(x / p), and that is the same period arithmetic FnNonDifferentiable had been naming as the one missing piece for the piecewise oscillators.

enum class TimeWarpKind(
    val eval: (Double, Double) -> Double,
    val glsl: (String, Double) -> String,
    val fn: RectSLFnLowering<TrackFnWarp>
) {

delay-time / time+ / ->live: play the source amount seconds later.

    SHIFT(
        { t, amount -> t - amount },
        { t, amount -> "($t - ${amount.glslFloat()})" },
        RectSLFnLowering.Lowered({ src, amount -> with(Functions) { src.shift(fnLeaf(amount)) } })
    ),

scale-time / time*: stretch the source's clock by amount.

    SCALE(
        { t, amount -> t / amount },
        { t, amount -> "($t / ${amount.glslFloat()})" },
        RectSLFnLowering.Lowered({ src, amount ->
            with(Functions) { src.scaleX(fnLeaf(amount), fnLeaf(0.0)) }
        })
    ),

reverse-time: amount is the scene duration, not a factor.

Mirroring and then delaying, in that order: scaleX(-1) looks up at -x, and the delay makes that -(x - amount). That is amount - x to the bit rather than to an ulp, because it is one exact negation of one subtraction — whereas mirroring about amount/2 would round twice.

    REVERSE(
        { t, amount -> amount - t },
        { t, amount -> "(${amount.glslFloat()} - $t)" },
        RectSLFnLowering.Lowered({ src, amount ->
            with(Functions) { src.scaleX(fnLeaf(-1.0), fnLeaf(0.0)).shift(fnLeaf(amount)) }
        })
    ),

loop-time: wrap into [0, amount).

Lowers as x - p·floor(x / p), which is positiveModulo with its sign branch turned into arithmetic. The whole reason afield.math.functions.Functions.Fn.floor exists is this line.

    LOOP(
        { t, amount -> positiveModulo(t, amount) },
        { t, amount ->
            if (amount <= 1e-12) "($t)" else "mod($t, ${amount.glslFloat()})"
        },
        RectSLFnLowering.Lowered({ src, amount ->
            if (amount <= 1e-12) {
                src
            } else {
                // A direct warp rather than fnLookupAt: `x + (-p)·floor(x/p)` is the floored modulo
                // in exactly one multiply and one add, with no `- x` round trip to lose bits on.
                with(Functions) {
                    src.warp(
                        mod = (fnTime() / fnLeaf(amount)).floor(),
                        depth = fnLeaf(-amount)
                    )
                }
            }
        })
    ),

pingpong-time: triangle wave over [0, amount], folded at both ends.

a - |mod(x, 2a) - a| — the same closed form the GLSL column already emits, which the SDF agreement test has been exercising against the CPU sampler all along.

    PINGPONG(
        { t, amount ->
            val p = positiveModulo(t, amount * 2.0)
            if (p <= amount) p else amount * 2.0 - p
        },
        { t, amount ->
            if (amount * 2.0 <= 1e-12) "($t)"
            else "(${amount.glslFloat()} - abs(mod($t, ${(amount * 2.0).glslFloat()}) - ${amount.glslFloat()}))"
        },
        RectSLFnLowering.Lowered({ src, amount ->
            if (amount * 2.0 <= 1e-12) {
                src
            } else {
                with(Functions) {
                    val folded = fnLeaf(amount) -
                        (fnFlooredModulo(fnTime(), amount * 2.0) - fnLeaf(amount)).abs()
                    fnLookupAt(src, folded)
                }
            }
        })
    )
}

sealed interface Track<out T>

val kind: TrackValueKind

fun sample(t: Double): T

data class ConstantTrack<T>

data class ConstantTrack<T>(
    val value: T,
    override val kind: TrackValueKind
) : Track<T> {

override fun sample(t: Double): T = value

data class TcbKey

One key of a keyed track: a time, and the value the track takes there.

The value is a Track rather than a bare number, and that is the whole of what lets a fit reach inside a keyed track rather than only compose around one. Any time-invariant track will do — a constant, a FreeParamTrack, or arithmetic over them — and that is checked at construction, because a key whose value varied with time would have no answer to "the value at this key" and would quietly mean whichever instant something happened to fold it at.

TcbKey(0.0, 1.0) still writes an ordinary constant key: the Double constructor is the common case, and the reason several hundred existing keys did not have to be respelled.

data class TcbKey(
    val time: Double,
    val valueTrack: Track<Double>,
    val tension: Double? = null,
    val continuity: Double? = null,
    val bias: Double? = null
) {

constructor

    constructor(
        time: Double,
        value: Double,
        tension: Double? = null,
        continuity: Double? = null,
        bias: Double? = null
    ) : this(time, constantTrack(value), tension, continuity, bias)

val value: Double

The number this key currently stands at — valueTrack folded once, at construction.

Every CPU sampler and the GLSL emitter read this and nothing else, so a parameterised key plays and renders exactly as the constant one it presently is, which is FreeParamTrack's whole contract. The differentiable backend is the only thing that looks at valueTrack.

    val value: Double = trackConstantValue(valueTrack)
        ?: throw RectLangException(
            "a key value has to be time-invariant — a constant, a free parameter, or arithmetic " +
                "over them — and this one varies with time: ${valueTrack.describe()}"
        )

val isConstantValue: Boolean

True when this key is a plain number, with no free parameter under it for a fit to move.

    val isConstantValue: Boolean
        get() = trackConstantValue(valueTrack, foldFreeParameters = false) != null
}

data class HermiteSegment

One cubic Hermite span of a TcbTrack, tangents already resolved.

data class HermiteSegment(
    val t0: Double,
    val t1: Double,
    val p0: Double,
    val p1: Double,
    val m0: Double,
    val m1: Double
) {

val dt: Double get() = t1 - t0

data class TcbTrack

data class TcbTrack(
    val keys: List<TcbKey>,
    val defaultTension: Double = 0.0,
    val defaultContinuity: Double = 0.0,
    val defaultBias: Double = 0.0
) : Track<Double> {

override val kind: TrackValueKind = TrackValueKind.Double

    private val sorted = keys.sortedBy { it.time }

init

    init {
        require(sorted.isNotEmpty()) { "TCB track needs at least one key" }
    }

override fun sample(t: Double): Double

    override fun sample(t: Double): Double {
        if (sorted.size == 1) return sorted.first().value
        if (t <= sorted.first().time) return sorted.first().value
        if (t >= sorted.last().time) return sorted.last().value
        val hi = sorted.indexOfFirst { it.time >= t }.coerceAtLeast(1)
        val lo = hi - 1
        val k0 = sorted[lo]
        val k1 = sorted[hi]
        val dt = (k1.time - k0.time).takeIf { abs(it) > 1e-12 } ?: return k1.value
        val u = ((t - k0.time) / dt).coerceIn(0.0, 1.0)
        val m0 = outgoingSlope(lo)
        val m1 = incomingSlope(hi)
        val u2 = u * u
        val u3 = u2 * u
        val h00 = 2.0 * u3 - 3.0 * u2 + 1.0
        val h10 = u3 - 2.0 * u2 + u
        val h01 = -2.0 * u3 + 3.0 * u2
        val h11 = u3 - u2
        return h00 * k0.value + h10 * dt * m0 + h01 * k1.value + h11 * dt * m1
    }

fun withTcb(tension: Double, continuity: Double, bias: Double): TcbTrack

    fun withTcb(tension: Double, continuity: Double, bias: Double): TcbTrack =
        copy(defaultTension = tension, defaultContinuity = continuity, defaultBias = bias)

fun hermiteSegments(): List<HermiteSegment>

The track as plain cubic Hermite segments, with the TCB tangents already resolved. The tension/continuity/bias parameters only ever influence the two slopes at a segment's ends, so once those are computed the curve is an ordinary Hermite spline — which is what makes a TCB track emittable as a handful of constants rather than as a solver.

body — 43 lines, from line 794
    fun hermiteSegments(): List<HermiteSegment> =
        (0 until sorted.size - 1).map { lo ->
            val hi = lo + 1
            HermiteSegment(
                t0 = sorted[lo].time,
                t1 = sorted[hi].time,
                p0 = sorted[lo].value,
                p1 = sorted[hi].value,
                m0 = outgoingSlope(lo),
                m1 = incomingSlope(hi)
            )
        }

    private fun outgoingSlope(index: Int): Double {
        val key = sorted[index]
        val t = key.tension ?: defaultTension
        val c = key.continuity ?: defaultContinuity
        val b = key.bias ?: defaultBias
        val prev = slope(index - 1, index)
        val next = slope(index, index + 1)
        return 0.5 * (1.0 - t) * ((1.0 + c) * (1.0 + b) * prev + (1.0 - c) * (1.0 - b) * next)
    }

    private fun incomingSlope(index: Int): Double {
        val key = sorted[index]
        val t = key.tension ?: defaultTension
        val c = key.continuity ?: defaultContinuity
        val b = key.bias ?: defaultBias
        val prev = slope(index - 1, index)
        val next = slope(index, index + 1)
        return 0.5 * (1.0 - t) * ((1.0 - c) * (1.0 + b) * prev + (1.0 + c) * (1.0 - b) * next)
    }

    private fun slope(a: Int, b: Int): Double {
        val ia = a.coerceIn(sorted.indices)
        val ib = b.coerceIn(sorted.indices)
        if (ia == ib) return 0.0
        val ka = sorted[ia]
        val kb = sorted[ib]
        val dt = kb.time - ka.time
        return if (abs(dt) <= 1e-12) 0.0 else (kb.value - ka.value) / dt
    }
}

data class LinearScalarTrack(val keys: List<TcbKey>) : Track<Double>

override val kind: TrackValueKind = TrackValueKind.Double

    private val sorted = keys.sortedBy { it.time }

init

    init {
        require(sorted.isNotEmpty()) { "Linear track needs at least one key" }
    }

override fun sample(t: Double): Double

    override fun sample(t: Double): Double {
        if (sorted.size == 1) return sorted.first().value
        if (t <= sorted.first().time) return sorted.first().value
        if (t >= sorted.last().time) return sorted.last().value
        val hi = sorted.indexOfFirst { it.time >= t }.coerceAtLeast(1)
        val lo = hi - 1
        val a = sorted[lo]
        val b = sorted[hi]
        val u = ((t - a.time) / (b.time - a.time)).coerceIn(0.0, 1.0)
        return a.value + (b.value - a.value) * u
    }
}

data class HoldScalarTrack(val keys: List<TcbKey>) : Track<Double>

override val kind: TrackValueKind = TrackValueKind.Double

    private val sorted = keys.sortedBy { it.time }

init

    init {
        require(sorted.isNotEmpty()) { "Hold track needs at least one key" }
    }

override fun sample(t: Double): Double

    override fun sample(t: Double): Double =
        sorted.lastOrNull { it.time <= t }?.value ?: sorted.first().value
}

data class EasedScalarTrack

A keyed track whose segment parameter is shaped by a named easing.

The curve is looked up from easeName rather than carried as a closure, so two tracks with the same keys and the same easing compare equal, and a backend can ask which curve this is instead of only being able to call it.

data class EasedScalarTrack(
    val keys: List<TcbKey>,
    val easeName: String
) : Track<Double> {

override val kind: TrackValueKind = TrackValueKind.Double

    private val sorted = keys.sortedBy { it.time }

val ease: (Double) -> Double = RECTSL_EASINGS[easeName]

    val ease: (Double) -> Double = RECTSL_EASINGS[easeName]
        ?: throw RectLangException("unknown easing '$easeName'")

init

    init {
        require(sorted.isNotEmpty()) { "$easeName track needs at least one key" }
    }

override fun sample(t: Double): Double

body — 53 lines, from line 891
    override fun sample(t: Double): Double {
        if (sorted.size == 1) return sorted.first().value
        if (t <= sorted.first().time) return sorted.first().value
        if (t >= sorted.last().time) return sorted.last().value
        val hi = sorted.indexOfFirst { it.time >= t }.coerceAtLeast(1)
        val lo = hi - 1
        val a = sorted[lo]
        val b = sorted[hi]
        val dt = (b.time - a.time).takeIf { abs(it) > 1e-12 } ?: return b.value
        val u = ease(((t - a.time) / dt).coerceIn(0.0, 1.0))
        return a.value + (b.value - a.value) * u
    }
}

private fun Track<Double>.mapKeys(
    word: String,
    transform: (TcbKey) -> TcbKey
): Track<Double> =
    when (this) {
        is TcbTrack -> copy(keys = keys.map(transform))
        is LinearScalarTrack -> copy(keys = keys.map(transform))
        is HoldScalarTrack -> copy(keys = keys.map(transform))
        is EasedScalarTrack -> copy(keys = keys.map(transform))
        else -> throw RectLangException("$word needs a direct keyed scalar track")
    }

private fun Track<Double>.filterKeys(
    word: String,
    predicate: (TcbKey) -> Boolean
): Track<Double> =
    when (this) {
        is TcbTrack -> {
            val filtered = keys.filter(predicate)
            if (filtered.isEmpty()) throw RectLangException("$word removed every key from TCB track")
            copy(keys = filtered)
        }
        is LinearScalarTrack -> {
            val filtered = keys.filter(predicate)
            if (filtered.isEmpty()) throw RectLangException("$word removed every key from linear track")
            copy(keys = filtered)
        }
        is HoldScalarTrack -> {
            val filtered = keys.filter(predicate)
            if (filtered.isEmpty()) throw RectLangException("$word removed every key from hold track")
            copy(keys = filtered)
        }
        is EasedScalarTrack -> {
            val filtered = keys.filter(predicate)
            if (filtered.isEmpty()) throw RectLangException("$word removed every key from eased track")
            copy(keys = filtered)
        }
        else -> throw RectLangException("$word needs a direct keyed scalar track")
    }

Fold a value transform into a directly-keyed track's key values, or — when the track is a computed node with no keys to reach — wrap it in the equivalent symbolic op. Callers supply both spellings because the keyed path wants a plain arithmetic transform and the wrapped path wants an IR node; they must agree, and each caller states the pair on one line so it is obvious when they do not.

source — 53 lines, from line 953
private fun Track<Double>.mapKeysOrSamples(
    word: String,
    transform: (Double) -> Double,
    wrap: (Track<Double>) -> Track<Double>
): Track<Double> =
    when (this) {
        is TcbTrack, is LinearScalarTrack, is HoldScalarTrack, is EasedScalarTrack ->
            mapKeys(word) { key ->
                // A constant key takes the plain arithmetic, so a scene with no parameters in it
                // folds to exactly the numbers it always did. A parameterised key takes the
                // symbolic spelling instead: folding `transform` into its current value would
                // delete the unknown, which is the one thing a key holding a track is for.
                if (key.isConstantValue) key.copy(valueTrack = constantTrack(transform(key.value)))
                else key.copy(valueTrack = wrap(key.valueTrack))
            }
        else -> wrap(this)
    }

private fun shiftTrackKeysOrWarp(track: Track<Double>, amount: Double): Track<Double> =
    when (track) {
        is TcbTrack, is LinearScalarTrack, is HoldScalarTrack, is EasedScalarTrack ->
            track.mapKeys("time+") { it.copy(time = it.time + amount) }
        else -> TimeWarpTrack(track, TimeWarpKind.SHIFT, amount)
    }

private fun scaleTrackKeysOrWarp(track: Track<Double>, factor: Double): Track<Double> {
    if (abs(factor) <= 1e-12) throw RectLangException("time* factor must be non-zero")
    return when (track) {
        is TcbTrack, is LinearScalarTrack, is HoldScalarTrack, is EasedScalarTrack ->
            track.mapKeys("time*") { it.copy(time = it.time * factor) }
        else -> TimeWarpTrack(track, TimeWarpKind.SCALE, factor)
    }
}

private fun Track<Double>.keyedValues(word: String): List<TcbKey> =
    when (this) {
        is TcbTrack -> keys
        is LinearScalarTrack -> keys
        is HoldScalarTrack -> keys
        is EasedScalarTrack -> keys
        else -> throw RectLangException("$word needs a direct keyed scalar track")
    }

private fun easeIn(u: Double): Double = u * u
private fun easeOut(u: Double): Double = 1.0 - (1.0 - u) * (1.0 - u)
private fun easeInOut(u: Double): Double {
    val v = -2.0 * u + 2.0
    return if (u < 0.5) 2.0 * u * u else 1.0 - v * v * 0.5
}
private fun smoothstep(u: Double): Double = u * u * (3.0 - 2.0 * u)

private fun easedTrack(word: String, track: Track<Double>, name: String = word): Track<Double> =
    EasedScalarTrack(track.keyedValues(word), name)

The full Penner easing family, keyed by kebab-case name (e.g. "back-out", "elastic-in-out", "smootherstep"). The math is borrowed from afield.math.functions.Functions so RectSL's ease reads identically to the rest of the procedural-animation toolkit rather than re-deriving the curves here.

private val PENNER_EASINGS: Map<String, (Double) -> Double> = buildMap {
    with(Functions) {
        for (e in Functions.Easing.values()) {
            val curve = identity().ease(e)
            put(e.name.lowercase().replace('_', '-')) { x: Double -> curve.eval(x) }
        }
    }
}

internal val RECTSL_EASINGS: Map<String, (Double) -> Double> = buildMap {

Every easing an EasedScalarTrack can name: the Penner family plus the four shorthand words (ease-in, ease-out, ease-in-out, ease-smoothstep / smooth) that predate it. One registry means an easing track is always reconstructible from its name alone.

internal val RECTSL_EASINGS: Map<String, (Double) -> Double> = buildMap {
    putAll(PENNER_EASINGS)
    put("ease-in", ::easeIn)
    put("ease-out", ::easeOut)
    put("ease-in-out", ::easeInOut)
    put("ease-smoothstep", ::smoothstep)
    put("smooth", ::smoothstep)
}

data class MappedDoubleTrack

data class MappedDoubleTrack(
    val source: Track<Double>,
    val op: TrackOp1
) : Track<Double> {

override val kind: TrackValueKind = TrackValueKind.Double

override fun sample(t: Double): Double = op.eval(source.sample(t))

data class BinaryDoubleTrack

data class BinaryDoubleTrack(
    val a: Track<Double>,
    val b: Track<Double>,
    val op: TrackOp2
) : Track<Double> {

override val kind: TrackValueKind = TrackValueKind.Double

override fun sample(t: Double): Double = op.eval(a.sample(t), b.sample(t))

data class TernaryDoubleTrack

data class TernaryDoubleTrack(
    val a: Track<Double>,
    val b: Track<Double>,
    val c: Track<Double>,
    val op: TrackOp3
) : Track<Double> {

override val kind: TrackValueKind = TrackValueKind.Double

override fun sample(t: Double): Double = op.eval(a.sample(t), b.sample(t), c.sample(t))

data class TimeWarpTrack

data class TimeWarpTrack(
    val source: Track<Double>,
    val warp: TimeWarpKind,
    val amount: Double
) : Track<Double> {

override val kind: TrackValueKind = TrackValueKind.Double

override fun sample(t: Double): Double = source.sample(warp.eval(t, amount))

data class GateTrack

A hard visibility window — 1.0 while lo <= t < hi, else 0.0. Multiplied into an object's alpha, it makes the object pop into and out of existence at fixed times (bullet spawn/despawn). lo/hi default to ±infinity for one-sided gates.

data class GateTrack(
    val lo: Double = Double.NEGATIVE_INFINITY,
    val hi: Double = Double.POSITIVE_INFINITY
) : Track<Double> {

override val kind: TrackValueKind = TrackValueKind.Double

override fun sample(t: Double): Double = if (t >= lo && t < hi) 1.0 else 0.0

data class HeadingTrack

Rotation derived from a pair of position tracks: the heading (direction of travel) of the point (x(t), y(t)). Because RectSL tracks are pure closed-form functions of time, the velocity is recovered by central finite difference — no simulation state — so a rect can auto-orient to its own trajectory. offset corrects for how the rect's art is drawn (0 = local +x points along travel).

At a velocity zero-crossing the heading is undefined; rather than pop, we widen the sampling window a few times looking for real motion, and fall back to the bare offset only if the point is genuinely stationary across the whole window.

data class HeadingTrack(
    val x: Track<Double>,
    val y: Track<Double>,
    val eps: Double = 1.0 / 240.0,
    val offset: Double = 0.0
) : Track<Double> {

override val kind: TrackValueKind = TrackValueKind.Double

override fun sample(t: Double): Double

    override fun sample(t: Double): Double {
        var h = eps
        repeat(6) {
            val dx = x.sample(t + h) - x.sample(t - h)
            val dy = y.sample(t + h) - y.sample(t - h)
            if (hypot(dx, dy) > 1e-9) return atan2(dy, dx) + offset
            h *= 4.0
        }
        return offset
    }
}

data class FreeParamTrack

A named unknown: a constant whose value is currently value but which is meant to be solved for rather than authored.

Every backend treats it as the constant it presently is — the CPU sampler and the shader both read value, so a scene with free parameters plays exactly as written. What changes is that the differentiable backend lowers it to a labelled Fn leaf, so freeParameterIndices can find which θ slot it landed in and Track.fitParameters can move that slot and nothing else. That is the whole mechanism behind inverse animation: say where a rect should be and when, and let the gradient answer with the numbers.

Written 4.0 "radius" param in RectSL.

data class FreeParamTrack(
    val name: String,
    val value: Double
) : Track<Double> {

override val kind: TrackValueKind = TrackValueKind.Double

override fun sample(t: Double): Double = value

fun constantTrack(x: Double): Track<Double> = ConstantTrack(x, TrackValueKind.Double)

fun constantPaintTrack(x: PaintSpec): Track<PaintSpec> = ConstantTrack(x, TrackValueKind.Paint)

enum class UnitKind

enum class UnitKind {
    Meters,
    Pixels,
    Abstract
}

enum class Origin

enum class Origin {
    Center,
    TopLeft
}

enum class FitPolicy

enum class FitPolicy {
    Contain,
    Cover,
    Stretch
}

data class WallSpec

data class WallSpec(
    val width: Double,
    val height: Double,
    val unit: UnitKind,
    val origin: Origin = Origin.Center
)

sealed interface PaintSpec

data class Solid(val color: Color4) : PaintSpec

data class LinearGradient

    data class LinearGradient(
        val c0: Color4,
        val c1: Color4,
        val p0: Vec2d,
        val p1: Vec2d,
        val space: GradientSpace = GradientSpace.Local
    ) : PaintSpec
}

enum class GradientSpace

enum class GradientSpace {
    Local,
    World
}

data class MaterialBinding

data class MaterialBinding(
    val materialId: Int,
    val shaderData: Vec4f = Vec4f.ZERO,
    val paramBase: Int = -1,
    val flags: Int = 0,
    val name: String = "plain"
)

sealed interface Node

val id: String

data class RectNode

data class RectNode(
    override val id: String,
    val x: Track<Double>,
    val y: Track<Double>,
    val w: Track<Double>,
    val h: Track<Double>,
    val rotation: Track<Double>,
    val scaleX: Track<Double>,
    val scaleY: Track<Double>,
    val alpha: Track<Double>,
    val z: Track<Double>,
    val fill: Track<PaintSpec>,
    val material: MaterialBinding,
    val tags: Set<String>
) : Node

data class GroupNode

data class GroupNode(
    override val id: String,
    val children: List<String>,
    val x: Track<Double> = constantTrack(0.0),
    val y: Track<Double> = constantTrack(0.0),
    val rotation: Track<Double> = constantTrack(0.0),
    val scaleX: Track<Double> = constantTrack(1.0),
    val scaleY: Track<Double> = constantTrack(1.0),
    val alpha: Track<Double> = constantTrack(1.0),
    val tags: Set<String> = emptySet()
) : Node

data class ShaderModuleSpec

data class ShaderModuleSpec(
    val name: String,
    val materialId: Int,
    val source: String,
    val prefix: String,
    val glsl: String? = null,
    val diagnostics: List<String> = emptyList()
)

data class MaterialTable

data class MaterialTable(
    val modules: List<ShaderModuleSpec> = emptyList(),
    val params: FloatArray = FloatArray(0)
)

data class SceneProgram

data class SceneProgram(
    val wall: WallSpec,
    val duration: Double,
    val fps: Double,
    val nodes: List<Node>,
    val fit: FitPolicy = FitPolicy.Contain,
    val background: Color4 = Color4.BLACK,
    val materialTable: MaterialTable = MaterialTable()
) {
    private val nodeMap: Map<String, Node> = nodes.associateBy { it.id }
    private val rectOrder: Map<String, Int> = nodes.filterIsInstance<RectNode>().mapIndexed { index, node -> node.id to index }.toMap()

fun sample(t: Double): SampledScene

body — 45 lines, from line 1247
    fun sample(t: Double): SampledScene {
        val childIds = nodes.filterIsInstance<GroupNode>().flatMap { it.children }.toSet()
        val roots = nodes.filter { it.id !in childIds }
        val rects = ArrayList<SampledRect>()
        roots.forEach { sampleNode(it, t, Transform2d.IDENTITY, rects) }
        return SampledScene(t, rects.sortedBy { it.z })
    }

    private fun sampleNode(node: Node, t: Double, parent: Transform2d, out: MutableList<SampledRect>) {
        when (node) {
            is RectNode -> {
                val local = Transform2d(
                    translation = Vec2d(node.x.sample(t), node.y.sample(t)),
                    rotation = node.rotation.sample(t),
                    scale = Vec2d(node.scaleX.sample(t), node.scaleY.sample(t)),
                    alpha = node.alpha.sample(t)
                )
                val world = parent then local
                out += SampledRect(
                    id = node.id,
                    objectId = rectOrder.getValue(node.id),
                    center = world.translation,
                    size = Vec2d(node.w.sample(t), node.h.sample(t)),
                    rotation = world.rotation,
                    scale = world.scale,
                    alpha = world.alpha.coerceIn(0.0, 1.0),
                    z = node.z.sample(t),
                    fill = node.fill.sample(t),
                    material = node.material,
                    tags = node.tags
                )
            }
            is GroupNode -> {
                val local = Transform2d(
                    translation = Vec2d(node.x.sample(t), node.y.sample(t)),
                    rotation = node.rotation.sample(t),
                    scale = Vec2d(node.scaleX.sample(t), node.scaleY.sample(t)),
                    alpha = node.alpha.sample(t)
                )
                val world = parent then local
                node.children.mapNotNull { nodeMap[it] }.forEach { sampleNode(it, t, world, out) }
            }
        }
    }
}

data class Transform2d

data class Transform2d(
    val translation: Vec2d,
    val rotation: Double,
    val scale: Vec2d,
    val alpha: Double
) {

infix fun then(child: Transform2d): Transform2d

    infix fun then(child: Transform2d): Transform2d {
        val scaled = Vec2d(child.translation.x * scale.x, child.translation.y * scale.y)
        val rotated = rotate(scaled, rotation)
        return Transform2d(
            translation = translation + rotated,
            rotation = rotation + child.rotation,
            scale = Vec2d(scale.x * child.scale.x, scale.y * child.scale.y),
            alpha = alpha * child.alpha
        )
    }

companion object

val IDENTITY = Transform2d(Vec2d(0.0, 0.0), 0.0, Vec2d(1.0, 1.0), 1.0)

data class SampledScene

data class SampledScene(
    val time: Double,
    val rects: List<SampledRect>
)

data class SampledRect

data class SampledRect(
    val id: String,
    val objectId: Int,
    val center: Vec2d,
    val size: Vec2d,
    val rotation: Double,
    val scale: Vec2d,
    val alpha: Double,
    val z: Double,
    val fill: PaintSpec,
    val material: MaterialBinding,
    val tags: Set<String>
)

data class RectVertex

data class RectVertex(
    val position: Vec4f,
    val color: Vec4f,
    val material: Vec4f,
    val shaderData: Vec4f
)

data class RectMesh

data class RectMesh(
    val vertices: List<RectVertex>,
    val indices: IntArray
)

data class RectMeshAttributeNames

data class RectMeshAttributeNames(
    val position: String = "a_position",
    val color: String = "a_color",
    val material: String = "a_material",
    val shaderData: String = "a_shader"
)

data class RectMaterialShaderCode

data class RectMaterialShaderCode(
    val insertion: String,
    val exampleFragmentShader: String,
    val compiledMaterials: List<StackSLRectMaterialCompileResult>
) {

val ok: Boolean get() = compiledMaterials.all { it.ok }

val diagnostics: List<String>

    val diagnostics: List<String>
        get() = compiledMaterials.flatMap { result ->
            result.stackSL.diagnostics.map { "${result.module.name}: $it" }
        }
}

data class FramePacket

data class FramePacket(
    val time: Double,
    val mesh: RectMesh,
    val materialTable: MaterialTable,
    val semantic: SemanticFrame
)

data class SemanticFrame

data class SemanticFrame(
    val rects: List<SampledRect>,
    val contacts: List<ContactPair>,
    val metrics: FrameSemanticMetrics
)

data class RectCollider

data class RectCollider(
    val id: String,
    val corners: Array<Vec2d>,
    val center: Vec2d,
    val axes: Array<Vec2d>,
    val halfExtents: Vec2d
)

data class ContactPair

data class ContactPair(
    val a: String,
    val b: String,
    val overlapDepth: Double
)

sealed interface VisualEvent

val time: Double

val confidence: Double

data class TouchStart

    data class TouchStart(
        override val time: Double,
        val a: String,
        val b: String,
        override val confidence: Double
    ) : VisualEvent

data class TouchEnd

    data class TouchEnd(
        override val time: Double,
        val a: String,
        val b: String,
        override val confidence: Double
    ) : VisualEvent

data class DirectionChange

    data class DirectionChange(
        override val time: Double,
        val id: String,
        val fromAngle: Double,
        val toAngle: Double,
        override val confidence: Double
    ) : VisualEvent

data class Stop

    data class Stop(
        override val time: Double,
        val id: String,
        override val confidence: Double
    ) : VisualEvent

data class StartMoving

    data class StartMoving(
        override val time: Double,
        val id: String,
        override val confidence: Double
    ) : VisualEvent

data class EnterScreen

    data class EnterScreen(
        override val time: Double,
        val id: String,
        override val confidence: Double
    ) : VisualEvent

data class LeaveScreen

    data class LeaveScreen(
        override val time: Double,
        val id: String,
        override val confidence: Double
    ) : VisualEvent

data class RotationReversal

    data class RotationReversal(
        override val time: Double,
        val id: String,
        override val confidence: Double
    ) : VisualEvent

data class OpacityCrossing

    data class OpacityCrossing(
        override val time: Double,
        val id: String,
        val threshold: Double,
        val rising: Boolean,
        override val confidence: Double
    ) : VisualEvent
}

data class MotionMetrics

data class MotionMetrics(
    val objectId: String,
    val time0: Double,
    val time1: Double,
    val dxMeters: Double,
    val dyMeters: Double,
    val speedMetersPerSecond: Double,
    val directionRadians: Double,
    val dRotationRadians: Double,
    val angularVelocityRadiansPerSecond: Double,
    val visibleAreaSquareMeters: Double,
    val opacityChange: Double
)

data class FrameSemanticMetrics

What one sampled frame shows. A rectangle counts as visible when it is not transparent and meets the wall, and the areas count only the part of it inside the wall (its bounding box's share, for a rotated one) — so a rectangle waiting below the wall to rise into it is not counted. Until 2026-09-27 every opaque rectangle counted wherever it was, which made a scene whose rectangles had all left the wall look fully occupied, and AnimationWarning.MostlyBlank never fire for it.

data class FrameSemanticMetrics(
    val time: Double,
    val visibleObjectCount: Int,
    val totalVisibleAreaSquareMeters: Double,
    val averageVelocity: Vec2d,
    val averageSpeedMetersPerSecond: Double,
    val averageAngularVelocityRadiansPerSecond: Double,
    val totalOpacityWeightedArea: Double
)

data class AnimationReport

data class AnimationReport(
    val duration: Double,
    val frameMetrics: List<FrameSemanticMetrics>,
    val objectMetrics: Map<String, List<MotionMetrics>>,
    val events: List<VisualEvent>,
    val warnings: List<AnimationWarning>
)

sealed interface AnimationWarning

data class MostlyBlank(val start: Double, val end: Double) : AnimationWarning

data class TooMuchChange(val time: Double, val amount: Double) : AnimationWarning

data class ObjectNeverVisible(val id: String) : AnimationWarning

data class ObjectLeavesTooEarly(val id: String, val time: Double) : AnimationWarning

data class ExcessiveOverlap(val time: Double, val ids: List<String>) : AnimationWarning

data class AspectMismatch(val wallAspect: Double, val renderAspect: Double) : AnimationWarning

data class AnalysisSettings

data class AnalysisSettings(
    val fps: Double? = null,
    val renderWidth: Double? = null,
    val renderHeight: Double? = null,
    val blankAreaThreshold: Double = 1e-4,
    val tooMuchChangeSpeed: Double = 8.0,
    val stopSpeed: Double = 0.002,
    val startMovingSpeed: Double = 0.006,
    val directionChangeRadians: Double = PI / 3.0,
    val opacityThresholds: List<Double> = listOf(0.01, 0.5)
)

data class ParseResult

data class ParseResult(
    val tokens: List<Token>,
    val diagnostics: List<String> = emptyList()
) {

val ok: Boolean get() = diagnostics.isEmpty()

data class CompileResult

data class CompileResult(
    val programOrNull: SceneProgram?,
    val diagnostics: List<String> = emptyList()
) {

val ok: Boolean get() = programOrNull != null && diagnostics.isEmpty()

val program: SceneProgram

The compiled program, or a RectLangException carrying the diagnostics if compilation failed. Prefer this (and the copy-paste-friendly .program!!, where the !! is now a harmless no-op) over reaching for programOrNull: a failed compile reports why — e.g. "Unknown word: arra" — instead of an opaque NullPointerException. Inspect ok/diagnostics first if you'd rather handle failure than throw.

    val program: SceneProgram
        get() = programOrNull ?: throw RectLangException(
            if (diagnostics.isEmpty()) "RectSL compilation failed"
            else "RectSL compilation failed:\n" + diagnostics.joinToString("\n")
        )
}

interface AnimationOracle

fun parse(source: String): ParseResult

fun compile(source: String): CompileResult

fun sample(program: SceneProgram, time: Double): SampledScene

fun lower(scene: SampledScene): RectMesh

fun analyze(program: SceneProgram, settings: AnalysisSettings = AnalysisSettings()): AnimationReport

    fun analyze(program: SceneProgram, settings: AnalysisSettings = AnalysisSettings()): AnimationReport

fun mutate(program: SceneProgram, edit: ProgramEdit): SceneProgram

sealed interface ProgramEdit

object RectAnimationOracle : AnimationOracle

override fun parse(source: String): ParseResult

    override fun parse(source: String): ParseResult =
        try {
            ParseResult(RectLangTokenizer(source).tokenize())
        } catch (t: Throwable) {
            ParseResult(emptyList(), listOf(t.message ?: t.toString()))
        }

override fun compile(source: String): CompileResult

    override fun compile(source: String): CompileResult =
        try {
            val tokens = RectLangTokenizer(source).tokenize()
            CompileResult(RectLangVm().eval(tokens))
        } catch (t: Throwable) {
            CompileResult(null, listOf(t.message ?: t.toString()))
        }

override fun sample(program: SceneProgram, time: Double): SampledScene = program.sample(time)

override fun lower(scene: SampledScene): RectMesh = lowerSampledScene(scene)

override fun analyze(program: SceneProgram, settings: AnalysisSettings): AnimationReport

    override fun analyze(program: SceneProgram, settings: AnalysisSettings): AnimationReport = analyzeAnimation(program, settings)

override fun mutate(program: SceneProgram, edit: ProgramEdit): SceneProgram = program

fun compileRectSL

Compile a RectSL program with host-supplied distribution curves bound by name. Each entry becomes a bare word that pushes its Functions.Fn (like a named track), so a program can sample a curve the host built — including one that fitBest/evolve optimised toward a target. This is the Functions→RectSL half of the bridge: the host searches curve-space, RectSL lays out the array from the winner.

fun compileRectSL(
    source: String,
    curves: Map<String, Functions.Fn> = emptyMap(),
    engine: KotlinScriptEngine? = null,
    precompiledEscapes: Map<String, (RectEscapeContext) -> Any?> = emptyMap()
): CompileResult =
    try {
        CompileResult(
            RectLangVm(namedCurves = curves, escapeEngine = engine, precompiledEscapes = precompiledEscapes)
                .eval(RectLangTokenizer(source).tokenize())
        )
    } catch (t: Throwable) {
        CompileResult(null, listOf(t.message ?: t.toString()))
    }

private fun escapeWrappedSource(source: String): String =
    "{ ctx: afield.lang.rectsl.RectEscapeContext -> with(afield.math.functions.Functions){ with(ctx){\n$source\n} } }"

@Suppress("UNCHECKED_CAST")
private fun castEscapeLambda(value: Any?, source: String): (RectEscapeContext) -> Any? =
    (value as? Function1<RectEscapeContext, Any?>)
        ?: throw RectLangException("{{ … }} did not evaluate to a lambda (got ${value?.javaClass?.name}): ${source.take(60)}")

private fun escapeSnippetValue(result: ResultValue?, source: String): Any? = when (result) {
    is ResultValue.Value -> result.value
    is ResultValue.Error -> throw RectLangException("{{ … }} threw while building: ${result.error.message}")
    else -> throw RectLangException("{{ … }} produced no lambda: ${source.take(60)}")
}

All distinct {{ … }} escape sources in a token stream, recursing into quotes.

private fun collectEscapeSources(tokens: List<Token>): List<String> {
    val out = LinkedHashSet<String>()
    fun walk(ts: List<Token>) {
        ts.forEach { t ->
            when (t) {
                is Token.KotlinEscape -> out += t.source
                is Token.Quote -> walk(t.tokens)
                else -> {}
            }
        }
    }
    walk(tokens)
    return out.toList()
}

suspend fun prewarmRectSLEscapes

Compile every {{ … }} escape in source to its lambda ahead of time, using the engine's suspend API so the compile runs off the caller's thread (on the compilation dispatcher) rather than blocking it. Call this before a graphics render loop — whose runloop is non-reentrant, so a lazy compileSnippetAndWait on first use during the render would deadlock — then hand the result to compileRectSL as precompiledEscapes, so the render-time compile is pure (only cached lambda invokes).

suspend fun prewarmRectSLEscapes(
    source: String,
    engine: KotlinScriptEngine
): Map<String, (RectEscapeContext) -> Any?> {
    // If the engine has an associated Box (e.g. a live box's own ki), pass it as the implicit
    // receiver so escapes can reference the box's state — and so the compiled snippet, whose
    // generated class expects that receiver, is invoked with it (an empty implicits list here
    // would eval with 0 args where 1 is expected). A bare engine has no associated box → empty.
    val implicits: List<Any> = listOfNotNull(engine.associatedBox)
    val out = LinkedHashMap<String, (RectEscapeContext) -> Any?>()
    collectEscapeSources(RectLangTokenizer(source).tokenize()).forEach { src ->
        val diagnostics = mutableListOf<String>()
        val snippet = engine.compileSnippet(escapeWrappedSource(src), implicits = implicits) { diags -> diags.forEach { diagnostics += it.message } }
            ?: throw RectLangException("{{ … }} did not compile: ${diagnostics.joinToString("; ").ifBlank { src.take(60) }}")
        out[src] = castEscapeLambda(escapeSnippetValue(snippet.eval(implicits = implicits, onEvalException = { throw it }), src), src)
    }
    return out
}

enum class RectWordEffect

enum class RectWordEffect {
    Ordinary,
    Mutator
}

data class RectWordMetadata

data class RectWordMetadata(
    val spelling: String,
    val effect: RectWordEffect
)

class Word

class Word(
    val metadata: RectWordMetadata,
    private val action: (RectLangVm) -> Unit
) {

fun apply(vm: RectLangVm) = action(vm)

data class LoopFrame

data class LoopFrame(
    val index: Int,
    val count: Int,
    val ordinal: Int = index,
    val col: Int = -1,
    val row: Int = -1,
    val cols: Int = -1,
    val rows: Int = -1,
    /**
     * Set only by `pat`: the musical identity of the event this copy came from, which is
     * what `t` / `dur` / `cyc` / `step` / `atom` / `param` read. It rides on `LoopFrame`
     * rather than a parallel stack so that `i`, `n`, `f`, `f0`, `fc` and `ic` keep working
     * inside a `pat` body exactly as they do inside `array`.
     */
    val pat: PatEventContext? = null
)

class RectLangVm

class RectLangVm(
    private val words: MutableMap<String, Word> = mutableMapOf(),
    private val scene: SceneBuilder = SceneBuilder(),
    private val namedCurves: Map<String, Functions.Fn> = emptyMap(),
    private val escapeEngine: KotlinScriptEngine? = null,
    precompiledEscapes: Map<String, (RectEscapeContext) -> Any?> = emptyMap()
) {
    private val stack = ArrayDeque<Value>()
    private val definitions = linkedMapOf<String, List<Token>>()
    private val namedTracks = linkedMapOf<String, Track<Double>>()
    private val escapeLambdas = linkedMapOf<String, (RectEscapeContext) -> Any?>().apply { putAll(precompiledEscapes) }

internal val loopStack = ArrayDeque<LoopFrame>()

    private var tokens: List<Token> = emptyList()
    private var pc: Int = 0
    private val evaluating = ArrayList<Token>()

var onLiteralTaken: ((token: Token, word: String, depth: Int) -> Unit)? = null

Told when a number or colour typed in the program is taken off the stack: its token, the word taking it, and how many values that word had already taken (0 for the top of the stack). What a study's knob labels rest on, in place of guessing from the tokens around a number; null costs nothing.

    private val literalTokens = java.util.IdentityHashMap<Value, Token>()
    private var takingWord: String? = null
    private var takenByWord = 0

var failedAt: Token?

The innermost token that was being evaluated when evaluation last failed, or null once an enclosing token finished normally (a word that caught the failure and carried on).

    var failedAt: Token? = null
        private set

fun evaluatingTokens(): List<Token> = evaluating.toList()

The tokens being evaluated right now, outermost first: a root-level word, the definition it runs, a word inside that, …

Plotting layer (rectsl-plot-axes)

A shared data→world coordinate frame so several series overlay in one domain, with axes/ticks/labels/legends. All fields default to null/0, so they are invisible to the classic chart words (bars/scatter/heat), which are left byte-identical — a hard compatibility requirement. See plotFrame, the frame/margins/domain words, and axes/line/area/points/colorbar.

    private var plotXDomain: Pair<Double, Double>? = null
    private var plotYDomain: Pair<Double, Double>? = null
    private var plotMarginL: Double = 0.0
    private var plotMarginR: Double = 0.0
    private var plotMarginB: Double = 0.0
    private var plotMarginT: Double = 0.0

init

    init {
        installCoreWords()
    }

fun wordMetadata(): List<RectWordMetadata>

    fun wordMetadata(): List<RectWordMetadata> =
        words.values.map { it.metadata }.distinctBy { it.spelling }.sortedBy { it.spelling }

fun eval(tokens: List<Token>): SceneProgram

    fun eval(tokens: List<Token>): SceneProgram {
        evalBlock(tokens)
        return scene.build()
    }

fun evalBlock(block: List<Token>)

    fun evalBlock(block: List<Token>) {
        val oldTokens = tokens
        val oldPc = pc
        tokens = block
        pc = 0
        try {
            while (pc < tokens.size) evalToken(tokens[pc++])
        } finally {
            tokens = oldTokens
            pc = oldPc
        }
    }

    private fun evalToken(token: Token) {
        evaluating += token
        try {
            when (token) {
                is Token.Number -> push(VNum(token.value).also { if (onLiteralTaken != null) literalTokens[it] = token })
                is Token.StringLit -> push(VStr(token.value))
                is Token.ColorLit -> push(VColor(token.color).also { if (onLiteralTaken != null) literalTokens[it] = token })
                is Token.Quote -> push(VQuote(token.tokens, token.source))
                is Token.KotlinEscape -> push(evalKotlinEscape(token.source))
                is Token.Word -> evalWord(token.name)
            }
            failedAt = null
        } catch (t: Throwable) {
            // The innermost evaluation catches first; enclosing ones keep what it recorded.
            if (failedAt == null) failedAt = token
            throw t
        } finally {
            evaluating.removeAt(evaluating.lastIndex)
        }
    }

Evaluate a {{ … }} escape and push the result. Each unique escape is compiled to a (RectEscapeContext) -> Any? lambda exactly once (a ~76 ms K2 compile + a ~5 ms eval-to-lambda), cached, then invoked per use as a plain JVM call — so an escape inside an array body pays the compile once, not per copy. Because it takes the array context, the Kotlin can read i/n/f/f0/fc/ic and vary per copy. Runs in a with(Functions){…} scope; the result maps to a Number, an Fn, or a Track<Double>. Needs a live Kotlin engine (the escapeEngine, or the current box's when run inside Field).

    private fun evalKotlinEscape(source: String): Value {
        val lambda = escapeLambdas.getOrPut(source) { compileEscape(source) }
        val result = try {
            lambda(currentEscapeContext())
        } catch (t: Throwable) {
            throw RectLangException("{{ … }} threw: ${t.message ?: t.toString()} — ${source.take(60)}")
        }
        return mapEscapeValue(result, source)
    }

Lazy (synchronous) compile — safe off a render loop (live editing, jvm-main, the smoke check). Inside a graphics render loop the runBlocking here would deadlock the non-reentrant runloop, so those paths must pre-warm via prewarmRectSLEscapes instead.

source — 85 lines, from line 1811
    private fun compileEscape(source: String): (RectEscapeContext) -> Any? {
        val engine = escapeEngine ?: Box.context.get()?.ki
            ?: throw RectLangException(
                "{{ … }} Kotlin escape needs a live Kotlin engine — run inside a Field box, or use " +
                    "compileRectSL(source, engine = …). Escape: ${source.take(60)}"
            )
        val diagnostics = mutableListOf<String>()
        val snippet = engine.compileSnippetAndWait(escapeWrappedSource(source)) { diags -> diags.forEach { diagnostics += it.message } }
            ?: throw RectLangException(
                "{{ … }} did not compile: ${diagnostics.joinToString("; ").ifBlank { source.take(60) }}"
            )
        return castEscapeLambda(escapeSnippetValue(snippet.evalAndWait(onEvalException = { throw it }), source), source)
    }

    private fun currentEscapeContext(): RectEscapeContext {
        val frame = loopStack.lastOrNull() ?: return RectEscapeContext(0.0, 1.0, 0.0, 0.0, 0.0, 0.0)
        val n = frame.count
        val fr = if (n <= 1) 0.0 else frame.ordinal.toDouble() / (n - 1).toDouble()
        return RectEscapeContext(
            i = frame.index.toDouble(),
            n = n.toDouble(),
            f = fr,
            f0 = if (n <= 0) 0.0 else frame.ordinal.toDouble() / n.toDouble(),
            fc = if (n <= 1) 0.0 else fr * 2.0 - 1.0,
            ic = frame.ordinal - (n - 1) / 2.0
        )
    }

    @Suppress("UNCHECKED_CAST")
    private fun mapEscapeValue(value: Any?, source: String): Value = when (value) {
        is Functions.Fn -> VFn(value)
        is Number -> VNum(value.toDouble())
        is Track<*> -> if (value.kind == TrackValueKind.Double) VTrack(value as Track<Double>)
        else throw RectLangException("{{ … }} returned Track<${value.kind}>, expected Track<Double>")
        else -> throw RectLangException(
            "{{ … }} must return a Number, an Fn, or a Track<Double>, but returned " +
                "${value?.let { it::class.simpleName ?: it.javaClass.name } ?: "null"}: ${source.take(60)}"
        )
    }

    private fun evalWord(name: String) {
        if (name == ":") {
            readDefinition()
            return
        }

        definitions[name]?.let {
            evalBlock(it)
            return
        }

        namedTracks[name]?.let {
            push(VTrack(it))
            return
        }

        namedCurves[name]?.let {
            push(VFn(it))
            return
        }

        if (name !in words && scene.hasMaterial(name)) {
            push(VMaterialRef(name))
            return
        }

        val word = words[name] ?: throw RectLangException(unknownWordMessage(name))
        val (outerWord, outerTaken) = takingWord to takenByWord
        takingWord = name
        takenByWord = 0
        try {
            word.apply(this)
        } finally {
            takingWord = outerWord
            takenByWord = outerTaken
        }
    }

    private fun unknownWordMessage(name: String): String {
        val arrowed = words["->$name"]
        if (arrowed?.metadata?.effect == RectWordEffect.Mutator) {
            return "Unknown word: $name (mutators use '->'; did you mean '->$name'?)"
        }
        return "Unknown word: $name" + suggestionFor(name)
    }

A "did you mean …?" tail for an unknown word — the vocabulary entries closest (by Levenshtein distance) to name. Catches the common typo, e.g. arra → array. Returns "" when nothing is near enough to be worth guessing.

    private fun suggestionFor(name: String): String {
        val vocabulary = words.keys + definitions.keys + namedTracks.keys + namedCurves.keys
        val threshold = if (name.length <= 4) 1 else 2
        val near = vocabulary
            .map { it to levenshtein(name, it, threshold) }
            .filter { it.second <= threshold }
            .sortedWith(compareBy({ it.second }, { it.first }))
            .map { it.first }
            .take(3)
        return if (near.isEmpty()) "" else " (did you mean ${near.joinToString(" or ") { "'$it'" }}?)"
    }

Edit distance between a and b, short-circuiting once it exceeds max.

    private fun levenshtein(a: String, b: String, max: Int): Int {
        if (kotlin.math.abs(a.length - b.length) > max) return max + 1
        var prev = IntArray(b.length + 1) { it }
        var curr = IntArray(b.length + 1)
        for (i in 1..a.length) {
            curr[0] = i
            var rowMin = curr[0]
            for (j in 1..b.length) {
                val cost = if (a[i - 1] == b[j - 1]) 0 else 1
                curr[j] = minOf(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost)
                if (curr[j] < rowMin) rowMin = curr[j]
            }
            if (rowMin > max) return max + 1
            val tmp = prev; prev = curr; curr = tmp
        }
        return prev[b.length]
    }

fun push(v: Value)

    fun push(v: Value) {
        stack.addLast(v)
    }

fun pop(): Value

    fun pop(): Value {
        if (stack.isEmpty()) throw RectLangException("Stack underflow")
        val value = stack.removeLast()
        onLiteralTaken?.let { hook ->
            val word = takingWord
            literalTokens.remove(value)?.let { token -> if (word != null) hook(token, word, takenByWord) }
            if (word != null) takenByWord++
        }
        return value
    }

fun peek(): Value

    fun peek(): Value =
        if (stack.isEmpty()) throw RectLangException("Stack underflow") else stack.last()

fun peekOrNull(): Value? = stack.lastOrNull()

fun stackValues(): List<Value> = stack.toList()

The stack after evaluation — lets tooling/tests extract values (e.g. a built VFn).

fun replaceStackRefs(oldId: String, newRef: Value)

    fun replaceStackRefs(oldId: String, newRef: Value) {
        val next = ArrayDeque<Value>()
        while (stack.isNotEmpty()) {
            next.addLast(
                when (val value = stack.removeFirst()) {
                    is VRect -> if (value.id == oldId && newRef is VRect) newRef else value
                    is VGroup -> if (value.id == oldId && newRef is VGroup) newRef else value
                    is VSelection -> VSelection(value.refs.map { ref ->
                        if (ref.id == oldId) {
                            when (newRef) {
                                is VRect -> NodeRef.Rect(newRef.id)
                                is VGroup -> NodeRef.Group(newRef.id)
                                else -> ref
                            }
                        } else ref
                    })
                    else -> value
                }
            )
        }
        stack.addAll(next)
    }

fun popNum(): Double

    fun popNum(): Double =
        (pop() as? VNum)?.x ?: throw RectLangException("Expected number")

fun popString(): String

    fun popString(): String =
        (pop() as? VStr)?.x ?: throw RectLangException("Expected string")

fun popQuote(): VQuote

    fun popQuote(): VQuote =
        pop() as? VQuote ?: throw RectLangException("Expected quote")

fun popFn(label: String): Functions.Fn

    fun popFn(label: String): Functions.Fn =
        (pop() as? VFn)?.fn ?: throw RectLangException("$label expected an Fn (curve)")

fun popData(label: String): List<Double>

    fun popData(label: String): List<Double> =
        (pop() as? VData)?.values ?: throw RectLangException("$label expected Data (a numeric list — build one with `data`, `linspace`, or `normalize`)")

fun stackDepth(): Int = stack.size

Current stack depth — mark it before an evalBlock, then drainNumbersAbove the mark.

fun drainNumbersAbove(mark: Int, label: String): List<Double>

Pop everything pushed above mark as a list of Numbers, preserving push order.

    fun drainNumbersAbove(mark: Int, label: String): List<Double> {
        val out = ArrayDeque<Double>()
        while (stack.size > mark) {
            val v = stack.removeLast()
            out.addFirst((v as? VNum)?.x
                ?: throw RectLangException("$label expected the quote to leave only Numbers, found ${v.describe()}"))
        }
        return out.toList()
    }

fun popColor(): Color4

    fun popColor(): Color4 = when (val v = pop()) {
        is VColor -> v.rgba
        is VNum -> Color4.fromHex(v.x.toLong())
        else -> throw RectLangException("Expected Color, found ${v.describe()}")
    }

fun popPaint(): PaintSpec

    fun popPaint(): PaintSpec = when (val v = pop()) {
        is VPaint -> v.paint
        else -> throw RectLangException("Expected Paint, found ${v.describe()}")
    }

fun popScalarTrackOrConstant(word: String): Track<Double> = scalarTrackFrom(pop(), word)

fun popBoolean(word: String): Boolean

    fun popBoolean(word: String): Boolean = when (val value = pop()) {
        is VNum -> abs(value.x) > 1e-12
        is VRect, is VGroup -> true
        is VSelection -> value.refs.isNotEmpty()
        else -> throw RectLangException("$word expected boolean-like Number, Rect, Group, or Selection, found ${value.describe()}")
    }

fun scalarTrackFrom(value: Value, word: String): Track<Double>

    @Suppress("UNCHECKED_CAST")
    fun scalarTrackFrom(value: Value, word: String): Track<Double> =
        when (value) {
            is VNum -> constantTrack(value.x)
            is VQuote -> quoteToScalarTrack(value, TrackMode.Tcb)
            is VTrack<*> -> {
                if (value.track.kind != TrackValueKind.Double) {
                    throw RectLangException("$word expected Number or Track<Double>, found Track<${value.track.kind}>")
                }
                value.track as Track<Double>
            }
            // The single coercion point in the language, which is why the musical layer only has
            // to touch it once: every track-taking mutator (->x ->y ->rot ->alpha ->w ->h ->scale
            // ->z ->layer …) and every scalar operator routes through here, so `sine ->alpha` and
            // `"0 0.25 0.5" mini nums ->rot` work without any of them knowing patterns exist.
            is VSig -> value.asTrack(scene.cps)
            is VPat -> value.asTrack(scene.cps)
            is VStr -> numericPatternTrackOrNull(value.x, scene.cps)
                ?: throw RectLangException(
                    "$word expected Number or Track<Double>, found String — a String is read as " +
                        "mini-notation only when it contains numbers, e.g. \"0 1 2 3\""
                )
            else -> throw RectLangException("$word expected Number or Track<Double>, found ${value.describe()}")
        }

fun pushUnaryScalar(word: String, op: TrackOp1)

    fun pushUnaryScalar(word: String, op: TrackOp1) {
        when (val value = pop()) {
            is VNum -> push(VNum(op.eval(value.x)))
            is VQuote, is VTrack<*> -> push(VTrack(MappedDoubleTrack(scalarTrackFrom(value, word), op)))
            else -> throw RectLangException("$word expected Number or Track<Double>, found ${value.describe()}")
        }
    }

fun pushBinaryScalar(word: String, op: TrackOp2)

    fun pushBinaryScalar(word: String, op: TrackOp2) {
        val b = pop()
        val a = pop()
        if (a is VNum && b is VNum) {
            push(VNum(op.eval(a.x, b.x)))
        } else {
            push(VTrack(BinaryDoubleTrack(scalarTrackFrom(a, word), scalarTrackFrom(b, word), op)))
        }
    }

fun pushTernaryScalar(word: String, op: TrackOp3)

    fun pushTernaryScalar(word: String, op: TrackOp3) {
        val c = pop()
        val b = pop()
        val a = pop()
        if (a is VNum && b is VNum && c is VNum) {
            push(VNum(op.eval(a.x, b.x, c.x)))
        } else {
            val at = scalarTrackFrom(a, word)
            val bt = scalarTrackFrom(b, word)
            val ct = scalarTrackFrom(c, word)
            push(VTrack(TernaryDoubleTrack(at, bt, ct, op)))
        }
    }

fun pushCompare(word: String, compareNum: (Double, Double) -> Boolean, compareStr: ((String, String) -> Boolean)? = null) {

    fun pushCompare(word: String, compareNum: (Double, Double) -> Boolean, compareStr: ((String, String) -> Boolean)? = null) {
        val b = pop()
        val a = pop()
        val result = when {
            a is VNum && b is VNum -> compareNum(a.x, b.x)
            a is VStr && b is VStr && compareStr != null -> compareStr(a.x, b.x)
            else -> throw RectLangException("$word expected comparable Numbers${if (compareStr != null) " or Strings" else ""}, found ${a.describe()} and ${b.describe()}")
        }
        push(VNum(if (result) 1.0 else 0.0))
    }

fun builder(): SceneBuilder = scene

fun defineTrack(name: String, track: Track<Double>)

    fun defineTrack(name: String, track: Track<Double>) {
        namedTracks[name] = track
    }

fun namedTrack(name: String): Track<Double>

    fun namedTrack(name: String): Track<Double> =
        namedTracks[name] ?: throw RectLangException("Unknown track '$name'")

fun withLoop(index: Int, count: Int, ordinal: Int = index, block: () -> Unit) {

    fun withLoop(index: Int, count: Int, ordinal: Int = index, block: () -> Unit) {
        loopStack.addLast(LoopFrame(index, count, ordinal))
        try {
            block()
        } finally {
            loopStack.removeLast()
        }
    }

fun withGrid(index: Int, count: Int, col: Int, row: Int, cols: Int, rows: Int, block: () -> Unit) {

    fun withGrid(index: Int, count: Int, col: Int, row: Int, cols: Int, rows: Int, block: () -> Unit) {
        loopStack.addLast(LoopFrame(index, count, index, col, row, cols, rows))
        try {
            block()
        } finally {
            loopStack.removeLast()
        }
    }

    private fun loopFrame(word: String): LoopFrame =
        loopStack.lastOrNull() ?: throw RectLangException("$word is only available inside repeat/range/array/grid/radial")

fun loopIndex(): Int = loopFrame("i").index

fun loopCount(): Int = loopFrame("count").count

fun loopFraction(): Double

Normalized fraction across the array, i/(n-1) in [0,1]; 0 for a single element.

    fun loopFraction(): Double {
        val f = loopFrame("f")
        return if (f.count <= 1) 0.0 else f.ordinal.toDouble() / (f.count - 1).toDouble()
    }

fun loopFraction0(): Double

Cyclic fraction ordinal/n in [0,1); handy for rings (angle = f0 tau *).

    fun loopFraction0(): Double {
        val f = loopFrame("f0")
        return if (f.count <= 0) 0.0 else f.ordinal.toDouble() / f.count.toDouble()
    }

fun loopFractionCentered(): Double

Centered fraction 2f-1 in [-1,1]; 0 for a single element.

    fun loopFractionCentered(): Double = if (loopFrame("fc").count <= 1) 0.0 else loopFraction() * 2.0 - 1.0

fun loopCenteredIndex(): Double

Centered index ordinal-(n-1)/2, symmetric around 0.

    fun loopCenteredIndex(): Double {
        val f = loopFrame("ic")
        return f.ordinal - (f.count - 1) / 2.0
    }

fun gridCol(): Int

    fun gridCol(): Int = loopFrame("col").col.also { if (it < 0) throw RectLangException("col is only available inside grid") }

fun gridRow(): Int

    fun gridRow(): Int = loopFrame("row").row.also { if (it < 0) throw RectLangException("row is only available inside grid") }

fun gridCols(): Int

    fun gridCols(): Int = loopFrame("cols").cols.also { if (it < 0) throw RectLangException("cols is only available inside grid") }

fun gridRows(): Int

    fun gridRows(): Int = loopFrame("rows").rows.also { if (it < 0) throw RectLangException("rows is only available inside grid") }

fun popTemplateRef(label: String): NodeRef

Template for array/radial: an explicit rect/group/id on the stack, else the active object.

    fun popTemplateRef(label: String): NodeRef {
        val top = peekOrNull()
        if (top is VRect || top is VGroup || top is VStr) return objectRefFromValue(pop())
        return scene.activeRef() ?: throw RectLangException("$label expected an active object or a rect/group on the stack")
    }

fun nextTokenOrNull(): Token? = tokens.getOrNull(pc)

fun consumeToken(): Token

    fun consumeToken(): Token =
        tokens.getOrNull(pc++) ?: throw RectLangException("Unexpected end of input")

fun consumeWordName(label: String): String

    fun consumeWordName(label: String): String =
        (consumeToken() as? Token.Word)?.name ?: throw RectLangException("$label expected word")

fun consumeName(label: String): String

    fun consumeName(label: String): String =
        when (val token = consumeToken()) {
            is Token.Word -> token.name
            is Token.StringLit -> token.value
            else -> throw RectLangException("$label expected word or string")
        }

fun consumeNumber(label: String): Double

    fun consumeNumber(label: String): Double =
        (consumeToken() as? Token.Number)?.value ?: throw RectLangException("$label expected number")

fun consumeQuote(label: String): VQuote

    fun consumeQuote(label: String): VQuote {
        val q = consumeToken() as? Token.Quote ?: throw RectLangException("$label expected quote")
        return VQuote(q.tokens, q.source)
    }

fun popObjectRefOrActive(): NodeRef

    fun popObjectRefOrActive(): NodeRef =
        scene.activeRef() ?: when (val value = pop()) {
            is VRect -> NodeRef.Rect(value.id)
            is VGroup -> NodeRef.Group(value.id)
            is VStr -> scene.refFor(value.x)
            else -> throw RectLangException("Expected rect or group reference, found ${value.describe()}")
        }

fun objectRefFromValue(value: Value): NodeRef

    fun objectRefFromValue(value: Value): NodeRef = when (value) {
        is VRect -> NodeRef.Rect(value.id)
        is VGroup -> NodeRef.Group(value.id)
        is VStr -> scene.refFor(value.x)
        else -> throw RectLangException("Expected rect or group reference, found ${value.describe()}")
    }

fun popSelectionOrActive(label: String): VSelection

    fun popSelectionOrActive(label: String): VSelection {
        val value = peekOrNull()
        if (value is VSelection || value is VRect || value is VGroup || value is VStr) {
            return selectionFrom(pop(), label)
        }
        val active = scene.activeRef() ?: throw RectLangException("$label expected Selection, Rect, Group, String id, or active object")
        return VSelection(listOf(active))
    }

fun selectionFrom(value: Value, label: String): VSelection

    fun selectionFrom(value: Value, label: String): VSelection =
        when (value) {
            is VSelection -> value
            is VRect -> VSelection(listOf(NodeRef.Rect(value.id)))
            is VGroup -> VSelection(listOf(NodeRef.Group(value.id)))
            is VStr -> VSelection(listOf(scene.refFor(value.x)))
            else -> throw RectLangException("$label expected Selection, Rect, Group, or String id, found ${value.describe()}")
        }

fun withSelected(ref: NodeRef, block: () -> Unit) {

body — 966 lines, from line 2219
    fun withSelected(ref: NodeRef, block: () -> Unit) {
        scene.withActive(ref, block)
    }

    private fun readDefinition() {
        val name = consumeWordName("Definition")
        val body = mutableListOf<Token>()
        while (pc < tokens.size) {
            val token = consumeToken()
            if (token is Token.Word && token.name == ";") {
                definitions[name] = body
                return
            }
            body += token
        }
        throw RectLangException("Definition '$name' is missing terminating ';'")
    }

    private fun installCoreWords() {
        word(";") { it.builder().endScope() }
        word("apply") { it.evalBlock(it.popQuote().tokens) }
        word("defer") { /* Quote literals are already deferred values. */ }

        word("dup") { vm ->
            val value = vm.peek()
            when (value) {
                is VRect, is VGroup -> vm.push(vm.builder().cloneNode(vm.objectRefFromValue(value)))
                is VSelection -> vm.push(vm.builder().cloneSelection(value.refs, deep = false))
                else -> vm.push(value)
            }
        }
        word("copy") { vm ->
            when (val value = vm.peek()) {
                is VSelection -> vm.push(vm.builder().cloneSelection(value.refs, deep = false))
                else -> vm.push(vm.builder().cloneNode(vm.objectRefFromValue(value)))
            }
        }
        word("clone") { vm ->
            when (val value = vm.peek()) {
                is VSelection -> vm.push(vm.builder().cloneSelection(value.refs, deep = false))
                else -> vm.push(vm.builder().cloneNode(vm.objectRefFromValue(value)))
            }
        }
        word("copy-deep") { vm ->
            when (val value = vm.peek()) {
                is VSelection -> vm.push(vm.builder().cloneSelection(value.refs, deep = true))
                else -> vm.push(vm.builder().cloneNodeDeep(vm.objectRefFromValue(value)))
            }
        }
        word("clone-deep") { vm ->
            when (val value = vm.peek()) {
                is VSelection -> vm.push(vm.builder().cloneSelection(value.refs, deep = true))
                else -> vm.push(vm.builder().cloneNodeDeep(vm.objectRefFromValue(value)))
            }
        }
        word("drop") { it.pop() }
        word("swap") { vm ->
            val a = vm.pop()
            val b = vm.pop()
            vm.push(a)
            vm.push(b)
        }
        word("over") { vm ->
            val a = vm.pop()
            val b = vm.peek()
            vm.push(a)
            vm.push(b)
        }
        word("pi") { it.push(VNum(PI)) }
        word("tau") { it.push(VNum(PI * 2.0)) }
        word("+") { it.pushBinaryScalar("+", TrackOp2.ADD) }
        word("add") { it.pushBinaryScalar("add", TrackOp2.ADD) }
        word("-") { it.pushBinaryScalar("-", TrackOp2.SUB) }
        word("sub") { it.pushBinaryScalar("sub", TrackOp2.SUB) }
        word("*") { it.pushBinaryScalar("*", TrackOp2.MUL) }
        word("mul") { it.pushBinaryScalar("mul", TrackOp2.MUL) }
        word("/") { it.pushBinaryScalar("/", TrackOp2.DIV) }
        word("div") { it.pushBinaryScalar("div", TrackOp2.DIV) }
        word("mod") { it.pushBinaryScalar("mod", TrackOp2.MOD) }
        word("pow") { it.pushBinaryScalar("pow", TrackOp2.POW) }
        word("min") { it.pushBinaryScalar("min", TrackOp2.MIN) }
        word("max") { it.pushBinaryScalar("max", TrackOp2.MAX) }
        word("neg") { it.pushUnaryScalar("neg", TrackOp1.NEG) }
        word("negate") { it.pushUnaryScalar("negate", TrackOp1.NEG) }
        word("abs") { it.pushUnaryScalar("abs", TrackOp1.ABS) }
        word("floor") { it.pushUnaryScalar("floor", TrackOp1.FLOOR) }
        word("ceil") { it.pushUnaryScalar("ceil", TrackOp1.CEIL) }
        word("fract") { it.pushUnaryScalar("fract", TrackOp1.FRACT) }
        word("sqrt") { it.pushUnaryScalar("sqrt", TrackOp1.SQRT) }
        word("sin") { it.pushUnaryScalar("sin", TrackOp1.SIN) }
        word("cos") { it.pushUnaryScalar("cos", TrackOp1.COS) }
        word("clamp") { it.pushTernaryScalar("clamp", TrackOp3.CLAMP) }
        word("sat") { it.pushUnaryScalar("sat", TrackOp1.SATURATE) }
        word("saturate") { it.pushUnaryScalar("saturate", TrackOp1.SATURATE) }
        word("mix") { it.pushTernaryScalar("mix", TrackOp3.MIX) }
        word("lerp") { it.pushTernaryScalar("lerp", TrackOp3.MIX) }
        word("smoothstep") { it.pushTernaryScalar("smoothstep", TrackOp3.SMOOTHSTEP) }
        word("<") { it.pushCompare("<", { a, b -> a < b }) }
        word("lt") { it.pushCompare("lt", { a, b -> a < b }) }
        word("<=") { it.pushCompare("<=", { a, b -> a <= b }) }
        word("lte") { it.pushCompare("lte", { a, b -> a <= b }) }
        word(">") { it.pushCompare(">", { a, b -> a > b }) }
        word("gt") { it.pushCompare("gt", { a, b -> a > b }) }
        word(">=") { it.pushCompare(">=", { a, b -> a >= b }) }
        word("gte") { it.pushCompare("gte", { a, b -> a >= b }) }
        word("=") { it.pushCompare("=", { a, b -> abs(a - b) <= 1e-12 }, { a, b -> a == b }) }
        word("eq") { it.pushCompare("eq", { a, b -> abs(a - b) <= 1e-12 }, { a, b -> a == b }) }
        word("!=") { it.pushCompare("!=", { a, b -> abs(a - b) > 1e-12 }, { a, b -> a != b }) }
        word("<>") { it.pushCompare("<>", { a, b -> abs(a - b) > 1e-12 }, { a, b -> a != b }) }
        word("neq") { it.pushCompare("neq", { a, b -> abs(a - b) > 1e-12 }, { a, b -> a != b }) }
        word("and") { vm ->
            val b = vm.popBoolean("and")
            val a = vm.popBoolean("and")
            vm.push(VNum(if (a && b) 1.0 else 0.0))
        }
        word("or") { vm ->
            val b = vm.popBoolean("or")
            val a = vm.popBoolean("or")
            vm.push(VNum(if (a || b) 1.0 else 0.0))
        }
        word("not") { vm -> vm.push(VNum(if (!vm.popBoolean("not")) 1.0 else 0.0)) }

        mutator("wall") { vm ->
            val width = vm.consumeNumber("wall")
            val height = vm.consumeNumber("wall")
            val unit = when (vm.consumeWordName("wall unit")) {
                "meters", "meter", "m" -> UnitKind.Meters
                "pixels", "pixel", "px" -> UnitKind.Pixels
                "abstract", "units", "unit" -> UnitKind.Abstract
                else -> throw RectLangException("wall expected unit meters, pixels, or abstract")
            }
            vm.builder().wall = WallSpec(width, height, unit)
        }
        mutator("duration") { it.builder().duration = it.consumeNumber("duration") }
        mutator("fps") { it.builder().fps = it.consumeNumber("fps") }
        mutator("background") { vm ->
            val value = vm.nextTokenOrNull()
            val color = if (value is Token.ColorLit) {
                vm.consumeToken()
                value.color
            } else vm.popColor()
            vm.builder().background = color
        }
        mutator("fit") { vm ->
            vm.builder().fit = when (vm.consumeWordName("fit")) {
                "contain" -> FitPolicy.Contain
                "cover" -> FitPolicy.Cover
                "stretch" -> FitPolicy.Stretch
                else -> throw RectLangException("fit expected contain, cover, or stretch")
            }
        }

        word("rect") { vm ->
            val id = (vm.peekOrNull() as? VStr)?.let { vm.popString() }
            vm.push(vm.builder().createRect(id))
        }
        word("group") { vm ->
            val id = (vm.peekOrNull() as? VStr)?.let { vm.popString() }
            vm.push(vm.builder().createGroup(id))
        }
        word("end") { it.builder().endGroup() }
        word("end-group") { it.builder().endGroup() }
        mutator("group-selected") { vm ->
            val id = (vm.peekOrNull() as? VStr)?.let { vm.popString() }
            val selection = vm.popSelectionOrActive("group-selected")
            vm.push(vm.builder().groupSelected(id, selection.refs))
        }
        mutator("select") { vm ->
            val ref = vm.objectRefFromValue(vm.pop())
            vm.builder().select(ref)
            vm.push(ref.toValue())
        }
        mutator("rename") { vm ->
            val newId = vm.popString()
            val ref = vm.builder().activeRef() ?: vm.objectRefFromValue(vm.pop())
            val renamed = vm.builder().rename(ref, newId)
            vm.replaceStackRefs(ref.id, renamed)
            vm.push(renamed)
        }
        mutator("tag") { vm -> vm.builder().tag(vm.popString()) }
        word("id") { vm -> vm.push(VStr(vm.builder().activeRef()?.id ?: throw RectLangException("id needs an active rect or group"))) }
        word("rect?") { vm -> vm.push(VNum(if (vm.builder().activeRef() is NodeRef.Rect) 1.0 else 0.0)) }
        word("group?") { vm -> vm.push(VNum(if (vm.builder().activeRef() is NodeRef.Group) 1.0 else 0.0)) }
        word("tagged?") { vm -> vm.push(VNum(if (vm.builder().hasTag(vm.popString())) 1.0 else 0.0)) }

        word("selected") { vm -> vm.push(VSelection(listOf(vm.builder().activeRef() ?: throw RectLangException("No active rect or group")))) }
        word("all") { vm -> vm.push(VSelection(vm.builder().allRefs())) }
        word("roots") { vm -> vm.push(VSelection(vm.builder().rootRefs())) }
        word("children") { vm -> vm.push(VSelection(vm.builder().childrenOf(vm.popSelectionOrActive("children").refs))) }
        word("descendants") { vm -> vm.push(VSelection(vm.builder().descendantsOf(vm.popSelectionOrActive("descendants").refs))) }
        word("rects") { vm ->
            val base = if (vm.peekOrNull() is VSelection || vm.peekOrNull() is VRect || vm.peekOrNull() is VGroup || vm.peekOrNull() is VStr) {
                vm.popSelectionOrActive("rects").refs
            } else vm.builder().allRefs()
            vm.push(VSelection(base.filterIsInstance<NodeRef.Rect>()))
        }
        word("groups") { vm ->
            val base = if (vm.peekOrNull() is VSelection || vm.peekOrNull() is VRect || vm.peekOrNull() is VGroup || vm.peekOrNull() is VStr) {
                vm.popSelectionOrActive("groups").refs
            } else vm.builder().allRefs()
            vm.push(VSelection(base.filterIsInstance<NodeRef.Group>()))
        }
        word("tagged") { vm ->
            val tag = vm.popString()
            val base = if (vm.peekOrNull() is VSelection || vm.peekOrNull() is VRect || vm.peekOrNull() is VGroup || vm.peekOrNull() is VStr) {
                vm.popSelectionOrActive("tagged").refs
            } else vm.builder().allRefs()
            vm.push(VSelection(base.filter { vm.builder().hasTag(it, tag) }))
        }

        mutator("xy") { vm ->
            val y = vm.popScalarTrackOrConstant("xy")
            val x = vm.popScalarTrackOrConstant("xy")
            vm.builder().setX(x)
            vm.builder().setY(y)
        }
        mutator("nxy") { vm ->
            val ny = vm.popScalarTrackOrConstant("nxy")
            val nx = vm.popScalarTrackOrConstant("nxy")
            val wall = vm.builder().wall
            // (n - 0.5) * extent, spelled with the same association as the scalar
            // form so the symbolic node is bit-identical to the old closure.
            vm.builder().setX(
                BinaryDoubleTrack(
                    BinaryDoubleTrack(nx, constantTrack(0.5), TrackOp2.SUB),
                    constantTrack(wall.width),
                    TrackOp2.MUL
                )
            )
            vm.builder().setY(
                BinaryDoubleTrack(
                    BinaryDoubleTrack(ny, constantTrack(0.5), TrackOp2.SUB),
                    constantTrack(wall.height),
                    TrackOp2.MUL
                )
            )
        }
        mutator("x") { vm -> vm.builder().setX(vm.popScalarTrackOrConstant("->x")) }
        mutator("y") { vm -> vm.builder().setY(vm.popScalarTrackOrConstant("->y")) }
        mutator("wh") { vm ->
            val h = vm.popScalarTrackOrConstant("wh")
            val w = vm.popScalarTrackOrConstant("wh")
            vm.builder().setW(w)
            vm.builder().setH(h)
        }
        mutator("nwh") { vm ->
            val nh = vm.popScalarTrackOrConstant("nwh")
            val nw = vm.popScalarTrackOrConstant("nwh")
            val wall = vm.builder().wall
            vm.builder().setW(BinaryDoubleTrack(nw, constantTrack(wall.width), TrackOp2.MUL))
            vm.builder().setH(BinaryDoubleTrack(nh, constantTrack(wall.height), TrackOp2.MUL))
        }
        mutator("w") { vm -> vm.builder().setW(vm.popScalarTrackOrConstant("->w")) }
        mutator("h") { vm -> vm.builder().setH(vm.popScalarTrackOrConstant("->h")) }
        mutator("square") { vm ->
            val side = vm.popScalarTrackOrConstant("square")
            vm.builder().setW(side)
            vm.builder().setH(side)
        }
        mutator("rot") { vm -> vm.builder().setRotation(vm.popScalarTrackOrConstant("->rot")) }
        mutator("rotation") { vm -> vm.builder().setRotation(vm.popScalarTrackOrConstant("->rotation")) }
        mutator("rotate") { vm -> vm.builder().addRotation(vm.popScalarTrackOrConstant("->rotate")) }
        mutator("face-travel") { vm -> vm.builder().faceTravel(0.0) }
        mutator("face-travel-at") { vm -> vm.builder().faceTravel(vm.popNum()) }
        mutator("scale") { vm ->
            val s = vm.popScalarTrackOrConstant("scale")
            vm.builder().setScaleX(s)
            vm.builder().setScaleY(s)
        }
        mutator("scale-xy") { vm ->
            val sy = vm.popScalarTrackOrConstant("scale-xy")
            val sx = vm.popScalarTrackOrConstant("scale-xy")
            vm.builder().setScaleX(sx)
            vm.builder().setScaleY(sy)
        }
        mutator("alpha") { vm -> vm.builder().setAlpha(vm.popScalarTrackOrConstant("->alpha")) }
        mutator("opacity*") { vm -> vm.builder().multiplyAlpha(vm.popScalarTrackOrConstant("->opacity*")) }
        mutator("appear") { vm -> vm.builder().gateVisibility(vm.popNum(), Double.POSITIVE_INFINITY) }
        mutator("vanish") { vm -> vm.builder().gateVisibility(Double.NEGATIVE_INFINITY, vm.popNum()) }
        mutator("live") { vm ->
            val life = vm.popNum()
            val birth = vm.popNum()
            vm.builder().live(birth, life)
        }
        word("x-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("x"))) }
        word("y-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("y"))) }
        word("w-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("w"))) }
        word("h-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("h"))) }
        word("rot-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("rot"))) }
        word("rotation-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("rot"))) }
        word("scale-x-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("scale-x"))) }
        word("scale-y-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("scale-y"))) }
        word("alpha-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("alpha"))) }
        word("z-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("z"))) }
        word("layer-track") { vm -> vm.push(VTrack(vm.builder().scalarTrack("layer"))) }

        mutator("z") { vm -> vm.builder().setZ(vm.popScalarTrackOrConstant("->z")) }
        mutator("layer") { vm ->
            when (val value = vm.pop()) {
                is VStr -> vm.builder().setLayer(value.x)
                is VNum -> vm.builder().setZ(constantTrack(value.x))
                is VTrack<*> -> vm.builder().setZ(vm.scalarTrackFrom(value, "layer"))
                is VQuote -> vm.builder().setZ(vm.scalarTrackFrom(value, "layer"))
                else -> throw RectLangException("layer expected name, Number, or Track<Double>, found ${value.describe()}")
            }
        }
        mutator("front") { it.builder().moveFront() }
        mutator("back") { it.builder().moveBack() }

        word("track") { vm ->
            val next = vm.nextTokenOrNull()
            if (vm.peekOrNull() == null && (next is Token.Word || next is Token.StringLit)) {
                val name = vm.consumeName("track")
                val quote = vm.consumeQuote("track")
                vm.defineTrack(name, vm.quoteToScalarTrack(quote, TrackMode.Tcb))
            } else {
                val quote = vm.popQuote()
                val name = vm.popString()
                vm.defineTrack(name, vm.quoteToScalarTrack(quote, TrackMode.Tcb))
            }
        }
        word("linear-track") { vm ->
            val name = vm.consumeName("linear-track")
            val quote = vm.consumeQuote("linear-track")
            vm.defineTrack(name, vm.quoteToScalarTrack(quote, TrackMode.Linear))
        }
        word("hold-track") { vm ->
            val name = vm.consumeName("hold-track")
            val quote = vm.consumeQuote("hold-track")
            vm.defineTrack(name, vm.quoteToScalarTrack(quote, TrackMode.Hold))
        }
        word("track-ref") { vm -> vm.push(VTrack(vm.namedTrack(vm.popString()))) }

        // A named unknown. `4.0 free-param radius` is a 4.0 that says it would rather be solved
        // for: every backend still reads it as the constant it presently is, so the scene plays
        // exactly as written, but the differentiable backend labels its Fn leaf so
        // [Track.fitParameters] can move that one number and nothing else. See RectSLFn.kt and
        // [FreeParamTrack]. Not spelled `param`: the pattern layer already owns that word for a
        // mini-notation parameter lookup.
        word("free-param") { vm ->
            val name = vm.consumeName("free-param")
            val initial = when (val v = vm.pop()) {
                is VNum -> v.x
                is VTrack<*> -> trackConstantValue(
                    @Suppress("UNCHECKED_CAST") (v.track as Track<Double>)
                ) ?: throw RectLangException(
                    "free-param needs a constant starting value, not a varying track"
                )
                else -> throw RectLangException("free-param expected a number")
            }
            vm.push(VTrack(FreeParamTrack(name, initial)))
        }

        word("rgb") { vm ->
            val r = vm.consumeNumber("rgb")
            val g = vm.consumeNumber("rgb")
            val b = vm.consumeNumber("rgb")
            vm.push(VColor(Color4(r, g, b)))
        }
        word("rgba") { vm ->
            val r = vm.consumeNumber("rgba")
            val g = vm.consumeNumber("rgba")
            val b = vm.consumeNumber("rgba")
            val a = vm.consumeNumber("rgba")
            vm.push(VColor(Color4(r, g, b, a)))
        }
        word("solid") { vm -> vm.push(VPaint(PaintSpec.Solid(vm.popColor()))) }
        word("linear") { vm ->
            if (vm.peekOrNull() is VQuote) {
                vm.push(VTrack(vm.quoteToScalarTrack(vm.popQuote(), TrackMode.Linear)))
            } else {
                val y1 = vm.popNum()
                val x1 = vm.popNum()
                val y0 = vm.popNum()
                val x0 = vm.popNum()
                val c1 = vm.popColor()
                val c0 = vm.popColor()
                vm.push(VPaint(PaintSpec.LinearGradient(c0, c1, Vec2d(x0, y0), Vec2d(x1, y1))))
            }
        }
        mutator("fill") { vm -> vm.builder().setFill(constantPaintTrack(vm.popPaint())) }

        word("tcb") { vm -> vm.push(VTrack(vm.quoteToScalarTrack(vm.popQuote(), TrackMode.Tcb))) }
        word("hold") { vm -> vm.push(VTrack(vm.quoteToScalarTrack(vm.popQuote(), TrackMode.Hold))) }
        word("tcb!") { vm ->
            val bias = vm.popNum()
            val continuity = vm.popNum()
            val tension = vm.popNum()
            val track = vm.popScalarTrackOrConstant("tcb!")
            val next = if (track is TcbTrack) track.withTcb(tension, continuity, bias) else track
            vm.push(VTrack(next))
        }
        word("delay-time") { vm ->
            val delay = vm.popNum()
            val track = vm.popScalarTrackOrConstant("delay-time")
            vm.push(VTrack(TimeWarpTrack(track, TimeWarpKind.SHIFT, delay)))
        }
        word("scale-time") { vm ->
            val factor = vm.popNum()
            val track = vm.popScalarTrackOrConstant("scale-time")
            vm.push(VTrack(TimeWarpTrack(track, TimeWarpKind.SCALE, factor)))
        }
        word("reverse-time") { vm ->
            val track = vm.popScalarTrackOrConstant("reverse-time")
            val end = vm.builder().duration
            vm.push(VTrack(TimeWarpTrack(track, TimeWarpKind.REVERSE, end)))
        }
        word("loop-time") { vm ->
            val period = vm.popNum()
            val track = vm.popScalarTrackOrConstant("loop-time")
            vm.push(VTrack(TimeWarpTrack(track, TimeWarpKind.LOOP, period)))
        }
        word("pingpong-time") { vm ->
            val period = vm.popNum()
            val track = vm.popScalarTrackOrConstant("pingpong-time")
            vm.push(VTrack(TimeWarpTrack(track, TimeWarpKind.PINGPONG, period)))
        }
        word("time+") { vm ->
            val amount = vm.popNum()
            val track = vm.popScalarTrackOrConstant("time+")
            vm.push(VTrack(shiftTrackKeysOrWarp(track, amount)))
        }
        word("time*") { vm ->
            val factor = vm.popNum()
            val track = vm.popScalarTrackOrConstant("time*")
            vm.push(VTrack(scaleTrackKeysOrWarp(track, factor)))
        }
        word("value+") { vm ->
            val amount = vm.popNum()
            val track = vm.popScalarTrackOrConstant("value+")
            vm.push(
                VTrack(
                    track.mapKeysOrSamples("value+", { it + amount }) {
                        BinaryDoubleTrack(it, constantTrack(amount), TrackOp2.ADD)
                    }
                )
            )
        }
        word("value*") { vm ->
            val factor = vm.popNum()
            val track = vm.popScalarTrackOrConstant("value*")
            vm.push(
                VTrack(
                    track.mapKeysOrSamples("value*", { it * factor }) {
                        BinaryDoubleTrack(it, constantTrack(factor), TrackOp2.MUL)
                    }
                )
            )
        }
        word("clamp-values") { vm ->
            val high = vm.popNum()
            val low = vm.popNum()
            val track = vm.popScalarTrackOrConstant("clamp-values")
            vm.push(
                VTrack(
                    track.mapKeysOrSamples("clamp-values", { it.coerceIn(min(low, high), max(low, high)) }) {
                        TernaryDoubleTrack(it, constantTrack(low), constantTrack(high), TrackOp3.CLAMP)
                    }
                )
            )
        }
        word("snap-time") { vm ->
            val grid = vm.popNum()
            val track = vm.popScalarTrackOrConstant("snap-time")
            if (abs(grid) <= 1e-12) throw RectLangException("snap-time grid must be non-zero")
            vm.push(VTrack(track.mapKeys("snap-time") { it.copy(time = round(it.time / grid) * grid) }))
        }
        word("keys-between") { vm ->
            val high = vm.popNum()
            val low = vm.popNum()
            val track = vm.popScalarTrackOrConstant("keys-between")
            val a = min(low, high)
            val b = max(low, high)
            vm.push(VTrack(track.filterKeys("keys-between") { it.time in a..b }))
        }
        word("keys-before") { vm ->
            val time = vm.popNum()
            val track = vm.popScalarTrackOrConstant("keys-before")
            vm.push(VTrack(track.filterKeys("keys-before") { it.time <= time }))
        }
        word("keys-after") { vm ->
            val time = vm.popNum()
            val track = vm.popScalarTrackOrConstant("keys-after")
            vm.push(VTrack(track.filterKeys("keys-after") { it.time >= time }))
        }
        word("ease-in") { vm -> vm.push(VTrack(easedTrack("ease-in", vm.popScalarTrackOrConstant("ease-in")))) }
        word("ease-out") { vm -> vm.push(VTrack(easedTrack("ease-out", vm.popScalarTrackOrConstant("ease-out")))) }
        word("ease-in-out") { vm -> vm.push(VTrack(easedTrack("ease-in-out", vm.popScalarTrackOrConstant("ease-in-out")))) }
        word("ease-smoothstep") { vm -> vm.push(VTrack(easedTrack("ease-smoothstep", vm.popScalarTrackOrConstant("ease-smoothstep")))) }
        word("smooth") { vm -> vm.push(VTrack(easedTrack("smooth", vm.popScalarTrackOrConstant("smooth")))) }
        mutator("delay-all-time") { vm ->
            val delay = vm.popNum()
            vm.builder().mapActiveScalarTracks { TimeWarpTrack(it, TimeWarpKind.SHIFT, delay) }
        }
        mutator("scale-all-time") { vm ->
            val factor = vm.popNum()
            vm.builder().mapActiveScalarTracks { TimeWarpTrack(it, TimeWarpKind.SCALE, factor) }
        }
        mutator("reverse-all-time") { vm ->
            val end = vm.builder().duration
            vm.builder().mapActiveScalarTracks { TimeWarpTrack(it, TimeWarpKind.REVERSE, end) }
        }
        mutator("loop-all-time") { vm ->
            val period = vm.popNum()
            vm.builder().mapActiveScalarTracks { TimeWarpTrack(it, TimeWarpKind.LOOP, period) }
        }
        mutator("pingpong-all-time") { vm ->
            val period = vm.popNum()
            vm.builder().mapActiveScalarTracks { TimeWarpTrack(it, TimeWarpKind.PINGPONG, period) }
        }
        mutator("stagger-time") { vm ->
            val step = vm.popNum()
            val selection = vm.popSelectionOrActive("stagger-time")
            vm.builder().staggerTime(selection.refs, step)
            vm.push(selection)
        }

        mutator("mirror-x") { it.builder().mirrorX(0.0) }
        mutator("mirror-y") { it.builder().mirrorY(0.0) }
        mutator("mirror-x-at") { vm -> vm.builder().mirrorX(vm.popNum()) }
        mutator("mirror-y-at") { vm -> vm.builder().mirrorY(vm.popNum()) }
        mutator("translate") { vm ->
            val dy = vm.popScalarTrackOrConstant("translate")
            val dx = vm.popScalarTrackOrConstant("translate")
            vm.builder().translate(dx, dy)
        }
        mutator("offset") { vm ->
            val dy = vm.popScalarTrackOrConstant("offset")
            val dx = vm.popScalarTrackOrConstant("offset")
            vm.builder().translate(dx, dy)
        }

        word("material") { vm ->
            val maybeName = vm.nextTokenOrNull()
            val maybeQuote = tokens.getOrNull(pc + 1)
            if (maybeName is Token.Word && maybeQuote is Token.Quote) {
                val name = vm.consumeWordName("material")
                val quote = vm.consumeQuote("material")
                vm.builder().defineStackSLMaterial(name, quote.source)
            } else {
                throw RectLangException("material assignment is a mutator; use '->material'")
            }
        }
        mutator("material") { vm ->
            val material = when (val v = vm.pop()) {
                is VMaterialRef -> v.name
                is VStr -> v.x
                else -> throw RectLangException("->material expected material name, found ${v.describe()}")
            }
            vm.builder().setMaterial(material)
        }
        mutator("shader") { vm ->
            val w = vm.popNum().toFloat()
            val z = vm.popNum().toFloat()
            val y = vm.popNum().toFloat()
            val x = vm.popNum().toFloat()
            vm.builder().setShaderData(Vec4f(x, y, z, w))
        }

        word("each") { vm ->
            val quote = vm.popQuote()
            val selection = vm.popSelectionOrActive("each")
            selection.refs.forEach { ref ->
                vm.withSelected(ref) { vm.evalBlock(quote.tokens) }
            }
            vm.push(selection)
        }
        word("map") { vm ->
            val quote = vm.popQuote()
            val selection = vm.popSelectionOrActive("map")
            selection.refs.forEach { ref ->
                vm.withSelected(ref) { vm.evalBlock(quote.tokens) }
            }
            vm.push(selection)
        }
        word("filter") { vm ->
            val quote = vm.popQuote()
            val selection = vm.popSelectionOrActive("filter")
            val filtered = selection.refs.filter { ref ->
                vm.withSelected(ref) { vm.evalBlock(quote.tokens) }
                vm.popBoolean("filter predicate")
            }
            vm.push(VSelection(filtered))
        }
        word("where") { vm ->
            val quote = vm.popQuote()
            val selection = vm.popSelectionOrActive("where")
            val filtered = selection.refs.filter { ref ->
                vm.withSelected(ref) { vm.evalBlock(quote.tokens) }
                vm.popBoolean("where predicate")
            }
            vm.push(VSelection(filtered))
        }
        word("times") { vm ->
            val quote = vm.popQuote()
            val count = vm.popNum().toInt().coerceAtLeast(0)
            repeat(count) { index ->
                vm.withLoop(index, count) {
                    vm.push(VNum(index.toDouble()))
                    vm.evalBlock(quote.tokens)
                }
            }
        }
        word("repeat") { vm ->
            val quote = vm.popQuote()
            val count = vm.popNum().toInt().coerceAtLeast(0)
            repeat(count) { index ->
                vm.withLoop(index, count) { vm.evalBlock(quote.tokens) }
            }
        }
        word("range") { vm ->
            val quote = vm.popQuote()
            val end = vm.popNum().toInt()
            val start = vm.popNum().toInt()
            val step = if (end >= start) 1 else -1
            val values = generateSequence(start) { it + step }
                .takeWhile { if (step > 0) it < end else it > end }
                .toList()
            values.forEachIndexed { ordinal, value ->
                vm.withLoop(value, values.size, ordinal) {
                    vm.evalBlock(quote.tokens)
                }
            }
        }
        word("i") { vm -> vm.push(VNum(vm.loopIndex().toDouble())) }
        word("loop-count") { vm -> vm.push(VNum(vm.loopCount().toDouble())) }
        word("n") { vm -> vm.push(VNum(vm.loopCount().toDouble())) }

        // --- Array primitives: reproduce a subexpression with per-copy modification ---
        // Extended per-copy context (valid inside repeat/range/array/grid/radial):
        word("f") { vm -> vm.push(VNum(vm.loopFraction())) }
        word("f0") { vm -> vm.push(VNum(vm.loopFraction0())) }
        word("fc") { vm -> vm.push(VNum(vm.loopFractionCentered())) }
        word("ic") { vm -> vm.push(VNum(vm.loopCenteredIndex())) }
        // Grid context (valid inside grid):
        word("col") { vm -> vm.push(VNum(vm.gridCol().toDouble())) }
        word("row") { vm -> vm.push(VNum(vm.gridRow().toDouble())) }
        word("cols") { vm -> vm.push(VNum(vm.gridCols().toDouble())) }
        word("rows") { vm -> vm.push(VNum(vm.gridRows().toDouble())) }
        word("colc") { vm -> vm.push(VNum(vm.gridCol() - (vm.gridCols() - 1) / 2.0)) }
        word("rowc") { vm -> vm.push(VNum(vm.gridRow() - (vm.gridRows() - 1) / 2.0)) }

        word("array") { vm ->
            val mods = vm.popQuote()
            val count = vm.popNum().toInt().coerceAtLeast(0)
            if (count == 0) {
                vm.push(VSelection(emptyList()))
                return@word
            }
            val template = vm.popTemplateRef("array")
            val refs = vm.builder().instantiateArray(template, count)
            refs.forEachIndexed { ordinal, ref ->
                vm.withSelected(ref) { vm.withLoop(ordinal, count, ordinal) { vm.evalBlock(mods.tokens) } }
            }
            vm.push(VSelection(refs))
        }
        word("radial") { vm ->
            val mods = vm.popQuote()
            val radius = vm.popNum()
            val count = vm.popNum().toInt().coerceAtLeast(0)
            if (count == 0) {
                vm.push(VSelection(emptyList()))
                return@word
            }
            val template = vm.popTemplateRef("radial")
            val refs = vm.builder().instantiateArray(template, count)
            refs.forEachIndexed { ordinal, ref ->
                vm.withSelected(ref) {
                    vm.withLoop(ordinal, count, ordinal) {
                        val angle = ordinal.toDouble() / count.toDouble() * (PI * 2.0)
                        vm.builder().setPolar(angle, radius)
                        vm.evalBlock(mods.tokens)
                    }
                }
            }
            vm.push(VSelection(refs))
        }
        word("arc") { vm ->
            val mods = vm.popQuote()
            val sweep = vm.popNum()
            val start = vm.popNum()
            val radius = vm.popNum()
            vm.placePolarArray("arc", mods) { ordinal, count ->
                val frac = if (count <= 1) 0.0 else ordinal.toDouble() / (count - 1).toDouble()
                (start + sweep * frac) to radius
            }
        }
        word("spiral") { vm ->
            val mods = vm.popQuote()
            val turns = vm.popNum()
            val radius1 = vm.popNum()
            val radius0 = vm.popNum()
            vm.placePolarArray("spiral", mods) { ordinal, count ->
                val frac = if (count <= 1) 0.0 else ordinal.toDouble() / (count - 1).toDouble()
                (frac * turns * PI * 2.0) to (radius0 + (radius1 - radius0) * frac)
            }
        }
        word("phyllotaxis") { vm -> vm.phyllotaxis() }
        word("sunflower") { vm -> vm.phyllotaxis() }
        word("grid") { vm ->
            val cell = vm.popQuote()
            val rows = vm.popNum().toInt().coerceAtLeast(0)
            val cols = vm.popNum().toInt().coerceAtLeast(0)
            val total = cols * rows
            val created = vm.builder().captureNewNodes {
                var index = 0
                for (r in 0 until rows) {
                    for (c in 0 until cols) {
                        vm.withGrid(index, total, c, r, cols, rows) { vm.evalBlock(cell.tokens) }
                        index++
                    }
                }
            }
            vm.push(VSelection(created))
        }
        mutator("split") { vm ->
            val mods = vm.popQuote()
            val atTime = vm.popNum()
            val count = vm.popNum().toInt().coerceAtLeast(0)
            val parent = vm.popTemplateRef("split")
            val b = vm.builder()
            // Seed shards from the still-pristine parent (before it is gated to death),
            // baking the parent's split-point position via build-time track sampling.
            val point = b.positionAt(parent, atTime)
            val shards = (0 until count).map { b.cloneShardAtOrigin(parent) }
            b.withActive(parent) { b.gateVisibility(Double.NEGATIVE_INFINITY, atTime) }
            val dur = b.duration
            shards.forEachIndexed { ordinal, shard ->
                // Author each shard's flight around the origin, in split-relative local time.
                // The mods may create sub-structure — including a nested `split` — so capture
                // every node they produce.
                val spawned = b.captureNewNodes {
                    vm.withSelected(shard) { vm.withLoop(ordinal, count, ordinal) { vm.evalBlock(mods.tokens) } }
                }
                // The shard's frame — its split point and birth time — applies to the shard
                // AND everything its mods produced. This is what makes `split` recursive: a
                // grand-shard's time adds onto its parent's (t1 then +t2) and its position is
                // offset onto this shard's split point.
                (listOf(shard) + spawned).forEach { node ->
                    b.withActive(node) {
                        b.translate(constantTrack(point.x), constantTrack(point.y))
                        b.live(atTime, dur - atTime)
                    }
                }
            }
            vm.push(VSelection(shards))
        }
        mutator("polar") { vm ->
            val radius = vm.popNum()
            val angle = vm.popNum()
            vm.builder().setPolar(angle, radius)
        }
        mutator("stagger") { vm ->
            val dy = vm.popNum()
            val dx = vm.popNum()
            val selection = vm.popSelectionOrActive("stagger")
            vm.builder().stagger(selection.refs, dx, dy)
            vm.push(selection)
        }

        // --- Functions.kt bridge: eased fractions + sampleable distribution curves ---
        // `ease` shapes a scalar/track through the full Penner family (single source
        // of truth: afield.math.functions.Functions). Curves (VFn) are graph-backed, so the
        // Functions rewrite/evolve tooling can optimise an array's distribution.
        word("ease") { vm ->
            val name = vm.popString().trim().lowercase().replace('_', '-')
            val ease = PENNER_EASINGS[name]
                ?: throw RectLangException(
                    "ease: unknown easing '$name' (try quad-out, cubic-in-out, sine-in-out, expo-out, " +
                        "circ-in-out, back-out, elastic-in-out, bounce-out, smoothstep, smootherstep)"
                )
            // A constant (e.g. the array fraction f) eases straight to a number; a
            // keyed track eases its interpolation like the ease-in/ease-out words.
            when (val v = vm.pop()) {
                is VNum -> vm.push(VNum(ease(v.x)))
                // Name the track with the curve that was actually asked for, not
                // with the word: "ease" alone would not say which curve it is.
                else -> vm.push(VTrack(easedTrack("ease", vm.scalarTrackFrom(v, "ease"), name)))
            }
        }
        // Distribution curves over [0,1] — sample at the array fraction f.
        word("gaussian") { vm -> vm.push(VFn(Functions.gaussian(mean = 0.5, sigma = 0.16, amplitude = 1.0))) }
        word("noise") { vm ->
            val seed = vm.popNum().toLong()
            vm.push(VFn(Functions.valueNoise(seed = seed, frequency = 8.0)))
        }
        word("fbm") { vm ->
            val seed = vm.popNum().toLong()
            vm.push(VFn(Functions.fbm(seed = seed, frequency = 4.0)))
        }
        word("sample") { vm ->
            val x = vm.popNum()
            val fn = vm.popFn("sample")
            vm.push(VNum(fn.eval(x)))
        }
        // Curve → curve transforms borrowed from the Functions pattern ops.
        word("reflect") { vm -> vm.push(VFn(Functions.mirror(0.5).invoke(vm.popFn("reflect")))) }
        word("tile") { vm ->
            val k = vm.popNum().coerceAtLeast(1.0)
            val fn = vm.popFn("tile")
            vm.push(VFn(with(Functions) { fn / repeatEvery(1.0 / k) }))
        }

        // --- Data visualization: perceptually-uniform colormaps ---------------------
        // `colormap`/`cmap` look up a named ramp (viridis, magma, inferno, plasma,
        // turbo, coolwarm, grayscale) at t in [0,1] and push a Color — usable anywhere
        // a colour is (solid/linear/fill/rgb slot). Reuses Color4/PaintSpec, no parallel
        // colour system. Single source of truth for the tables: [Colormaps].
        word("colormap") { vm ->
            val t = vm.popNum()
            val name = vm.popString()
            vm.push(VColor(Colormaps.sample(name, t)))
        }
        word("cmap") { vm ->
            val t = vm.popNum()
            val name = vm.popString()
            vm.push(VColor(Colormaps.sample(name, t)))
        }

        // --- Data ingestion + normalization -----------------------------------------
        // A `VData` is a plain numeric list. `data` runs a quote and collects every
        // Number it leaves (so `'( 3 1 4 1 5 ) data` or a computed `'( 8 times [ i i * ] )
        // data` both work); `linspace` generates evenly spaced values.
        word("data") { vm ->
            val quote = vm.popQuote()
            val mark = vm.stackDepth()
            vm.evalBlock(quote.tokens)
            vm.push(VData(vm.drainNumbersAbove(mark, "data")))
        }
        word("linspace") { vm ->
            val count = vm.popNum().toInt().coerceAtLeast(0)
            val hi = vm.popNum()
            val lo = vm.popNum()
            val values = when {
                count <= 0 -> emptyList()
                count == 1 -> listOf(lo)
                else -> (0 until count).map { lo + (hi - lo) * it / (count - 1).toDouble() }
            }
            vm.push(VData(values))
        }
        word("data-len") { vm -> vm.push(VNum(vm.popData("data-len").size.toDouble())) }
        word("data-at") { vm ->
            val index = vm.popNum().toInt()
            val d = vm.popData("data-at")
            if (d.isEmpty()) throw RectLangException("data-at: empty data")
            vm.push(VNum(d[index.coerceIn(0, d.size - 1)]))
        }
        word("extent") { vm ->
            val d = vm.popData("extent")
            if (d.isEmpty()) throw RectLangException("extent: empty data")
            vm.push(VNum(d.minOrNull()!!))
            vm.push(VNum(d.maxOrNull()!!))
        }
        word("dmin") { vm ->
            val d = vm.popData("dmin")
            vm.push(VNum(d.minOrNull() ?: throw RectLangException("dmin: empty data")))
        }
        word("dmax") { vm ->
            val d = vm.popData("dmax")
            vm.push(VNum(d.maxOrNull() ?: throw RectLangException("dmax: empty data")))
        }
        word("normalize") { vm -> vm.push(VData(normalizeValues(vm.popData("normalize")))) }
        word("data-map") { vm ->
            val quote = vm.popQuote()
            val d = vm.popData("data-map")
            val out = d.mapIndexed { index, value ->
                var result = 0.0
                vm.withLoop(index, d.size, index) {
                    vm.push(VNum(value))
                    vm.evalBlock(quote.tokens)
                    result = vm.popNum()
                }
                result
            }
            vm.push(VData(out))
        }

        // --- Chart idioms: produce plain rects + colormap colours -------------------
        // These are pure RectSL builders — no renderer changes. Values are normalized by
        // their own extent for both size and colour; the returned Selection can be
        // animated/transformed like any other (alpha, stagger-time, group, …).
        word("bars") { vm ->
            val name = vm.popString()
            val data = vm.popData("bars")
            val stops = Colormaps.stops(name)
            val wall = vm.builder().wall
            val n = data.size
            val nv = normalizeValues(data)
            val slot = if (n > 0) wall.width / n else wall.width
            val refs = ArrayList<NodeRef>(n)
            data.indices.forEach { i ->
                val rect = vm.builder().createRect("bar-$i")
                val height = (nv[i] * wall.height * 0.9).coerceAtLeast(wall.height * 0.002)
                vm.builder().setX(constantTrack(-wall.width / 2.0 + slot * (i + 0.5)))
                vm.builder().setY(constantTrack(-wall.height / 2.0 + height / 2.0))
                vm.builder().setW(constantTrack(slot * 0.8))
                vm.builder().setH(constantTrack(height))
                vm.builder().setFill(constantPaintTrack(PaintSpec.Solid(Colormaps.sampleStops(stops, nv[i]))))
                refs += NodeRef.Rect(rect.id)
                vm.builder().endScope()
            }
            vm.push(VSelection(refs))
        }
        word("heat") { vm -> vm.heatmap() }
        word("heatmap") { vm -> vm.heatmap() }
        word("scatter") { vm ->
            val name = vm.popString()
            val ydata = vm.popData("scatter")
            val xdata = vm.popData("scatter")
            val stops = Colormaps.stops(name)
            val wall = vm.builder().wall
            val n = min(xdata.size, ydata.size)
            val nx = normalizeValues(xdata.take(n))
            val ny = normalizeValues(ydata.take(n))
            val dot = min(wall.width, wall.height) * 0.035
            val refs = ArrayList<NodeRef>(n)
            for (i in 0 until n) {
                val rect = vm.builder().createRect("point-$i")
                vm.builder().setX(constantTrack((nx[i] - 0.5) * wall.width * 0.92))
                vm.builder().setY(constantTrack((ny[i] - 0.5) * wall.height * 0.92))
                vm.builder().setW(constantTrack(dot))
                vm.builder().setH(constantTrack(dot))
                vm.builder().setFill(constantPaintTrack(PaintSpec.Solid(Colormaps.sampleStops(stops, ny[i]))))
                refs += NodeRef.Rect(rect.id)
                vm.builder().endScope()
            }
            vm.push(VSelection(refs))
        }

        // --- Plotting layer: shared domain + axes + legend + domain-aware series -----
        // Everything below is a PURE RectSL builder (rects only, no renderer changes),
        // reusing createRect/PaintSpec/Colormaps exactly like the chart words above. The
        // classic bars/heat/scatter words are deliberately left untouched, so existing
        // programs — which never set a domain or margins — are byte-identical. A shared
        // data→world frame lets several series overlay in one coordinate system: the
        // first series (or an explicit `domain`) fixes the domain, later series + `axes`
        // + `colorbar` reuse it. There is no text primitive in RectSL, so numeric tick
        // labels are drawn with a 3x5 dot-matrix numeral font built from rects.
        mutator("margins") { vm ->
            val t = vm.popNum(); val b = vm.popNum(); val r = vm.popNum(); val l = vm.popNum()
            vm.setPlotMargins(l, r, b, t)
        }
        mutator("frame") { vm -> vm.applyDefaultFrame() }
        mutator("domain") { vm ->
            val y1 = vm.popNum(); val y0 = vm.popNum(); val x1 = vm.popNum(); val x0 = vm.popNum()
            vm.setXDomain(x0, x1); vm.setYDomain(y0, y1)
        }
        mutator("xdomain") { vm ->
            val x1 = vm.popNum(); val x0 = vm.popNum(); vm.setXDomain(x0, x1)
        }
        mutator("ydomain") { vm ->
            val y1 = vm.popNum(); val y0 = vm.popNum(); vm.setYDomain(y0, y1)
        }
        word("axes") { vm -> vm.drawAxes() }
        word("line") { vm -> vm.lineSeries() }
        word("plot") { vm -> vm.lineSeries() }
        word("area") { vm -> vm.areaSeries() }
        word("points") { vm -> vm.pointsSeries() }
        word("colorbar") { vm -> vm.drawColorbar() }
        word("legend") { vm -> vm.drawLegend() }

        // The musical layer — clock, `pat`, pattern-backed tracks, and the variation
        // combinators — lives in RectSLPattern.kt. See its file header for why it is a
        // separate file and what it borrows from afield.audio.synth.
        installPatternWords()
    }

Shared engine for the polar array generators (arc, spiral, phyllotaxis): pop count and the template, instantiate the copies, and for each place it at a per-ordinal (angle, radius) before running the per-copy mods with array context. radial predates this and keeps its own inline copy for byte-stability.

    private fun placePolarArray(word: String, mods: VQuote, placement: (ordinal: Int, count: Int) -> Pair<Double, Double>) {
        val count = popNum().toInt().coerceAtLeast(0)
        if (count == 0) {
            push(VSelection(emptyList()))
            return
        }
        val template = popTemplateRef(word)
        val refs = builder().instantiateArray(template, count)
        refs.forEachIndexed { ordinal, ref ->
            withSelected(ref) {
                withLoop(ordinal, count, ordinal) {
                    val (angle, radius) = placement(ordinal, count)
                    builder().setPolar(angle, radius)
                    evalBlock(mods.tokens)
                }
            }
        }
        push(VSelection(refs))
    }

`template count scale quote` → golden-angle sunflower: angle = i·137.5°, radius = scale·√i.

    private fun phyllotaxis() {
        val mods = popQuote()
        val scale = popNum()
        placePolarArray("phyllotaxis", mods) { ordinal, _ ->
            (ordinal * 2.399963229728653) to (scale * sqrt(ordinal.toDouble()))
        }
    }

Grid heatmap: data cols rows name heat → row-major cells (row 0 at top).

    private fun heatmap() {
        val name = popString()
        val rows = popNum().toInt().coerceAtLeast(1)
        val cols = popNum().toInt().coerceAtLeast(1)
        val data = popData("heat")
        val stops = Colormaps.stops(name)
        val wall = builder().wall
        val nv = normalizeValues(data)
        val cellW = wall.width / cols
        val cellH = wall.height / rows
        val refs = ArrayList<NodeRef>(cols * rows)
        for (r in 0 until rows) {
            for (c in 0 until cols) {
                val index = r * cols + c
                val value = if (nv.isEmpty()) 0.0 else nv[index % nv.size]
                val rect = builder().createRect("cell-$c-$r")
                builder().setX(constantTrack(-wall.width / 2.0 + cellW * (c + 0.5)))
                builder().setY(constantTrack(wall.height / 2.0 - cellH * (r + 0.5)))
                builder().setW(constantTrack(cellW * 0.98))
                builder().setH(constantTrack(cellH * 0.98))
                builder().setFill(constantPaintTrack(PaintSpec.Solid(Colormaps.sampleStops(stops, value))))
                refs += NodeRef.Rect(rect.id)
                builder().endScope()
            }
        }
        push(VSelection(refs))
    }

Plotting layer helpers

A shared data→world frame + axes/ticks/labels/legends, all built from rects.

    private var plotRectSeq = 0

World-space rectangle of the plotting area; equals the whole wall when no margins are set (wall is centred at origin, +y up).

    private fun plotFrame(): PlotFrame {
        val wall = builder().wall
        return PlotFrame(
            left = -wall.width / 2.0 + plotMarginL * wall.width,
            right = wall.width / 2.0 - plotMarginR * wall.width,
            bottom = -wall.height / 2.0 + plotMarginB * wall.height,
            top = wall.height / 2.0 - plotMarginT * wall.height
        )
    }

    private fun safeSpan(lo: Double, hi: Double): Pair<Double, Double> {
        if (!lo.isFinite() || !hi.isFinite()) return 0.0 to 1.0
        if (hi - lo <= 1e-9) { val c = (lo + hi) / 2.0; return (c - 0.5) to (c + 0.5) }
        return lo to hi
    }

    private fun setXDomain(x0: Double, x1: Double) { plotXDomain = safeSpan(min(x0, x1), max(x0, x1)) }
    private fun setYDomain(y0: Double, y1: Double) { plotYDomain = safeSpan(min(y0, y1), max(y0, y1)) }

Resolve the x domain, lazily adopting fallback (a series' own extent) so the first plotted series fixes the shared domain and later series + axes reuse it.

    private fun resolveXDomain(fallback: Pair<Double, Double>): Pair<Double, Double> =
        plotXDomain ?: safeSpan(fallback.first, fallback.second).also { plotXDomain = it }

    private fun resolveYDomain(fallback: Pair<Double, Double>): Pair<Double, Double> =
        plotYDomain ?: safeSpan(fallback.first, fallback.second).also { plotYDomain = it }

    private fun setPlotMargins(l: Double, r: Double, b: Double, t: Double) {
        plotMarginL = l.coerceIn(0.0, 0.45); plotMarginR = r.coerceIn(0.0, 0.45)
        plotMarginB = b.coerceIn(0.0, 0.45); plotMarginT = t.coerceIn(0.0, 0.45)
    }

frame — sensible default margins (room for axis labels left/bottom, a colorbar on the right), applied only when none are set so an explicit margins wins.

    private fun applyDefaultFrame() {
        if (plotMarginL == 0.0 && plotMarginR == 0.0 && plotMarginB == 0.0 && plotMarginT == 0.0) {
            plotMarginL = 0.12; plotMarginR = 0.14; plotMarginB = 0.12; plotMarginT = 0.06
        }
    }

    private fun minMax(v: List<Double>): Pair<Double, Double> =
        if (v.isEmpty()) 0.0 to 1.0 else (v.min() to v.max())

One filled rect in the plot; records its ref and closes the builder scope.

    private fun plotRect(
        idPrefix: String, cx: Double, cy: Double, w: Double, h: Double,
        color: Color4, refs: MutableList<NodeRef>, rotation: Double = 0.0
    ) {
        if (w <= 0.0 || h <= 0.0 || !cx.isFinite() || !cy.isFinite()) return
        val rect = builder().createRect("$idPrefix-${plotRectSeq++}")
        builder().setX(constantTrack(cx))
        builder().setY(constantTrack(cy))
        builder().setW(constantTrack(w))
        builder().setH(constantTrack(h))
        if (rotation != 0.0) builder().setRotation(constantTrack(rotation))
        builder().setFill(constantPaintTrack(PaintSpec.Solid(color)))
        refs.add(NodeRef.Rect(rect.id))
        builder().endScope()
    }

Heckbert's "nice numbers" tick algorithm — a compact, standard choice of round tick values covering [lo, hi] with about target ticks.

    private fun niceNum(x: Double, round: Boolean): Double {
        if (x <= 0.0) return 0.0
        val exp = floor(log10(x))
        val f = x / 10.0.pow(exp)
        val nf = if (round) {
            when { f < 1.5 -> 1.0; f < 3.0 -> 2.0; f < 7.0 -> 5.0; else -> 10.0 }
        } else {
            when { f <= 1.0 -> 1.0; f <= 2.0 -> 2.0; f <= 5.0 -> 5.0; else -> 10.0 }
        }
        return nf * 10.0.pow(exp)
    }

    private fun niceTicks(lo: Double, hi: Double, target: Int = 5): List<Double> {
        if (!lo.isFinite() || !hi.isFinite() || hi <= lo) return listOf(lo)
        val step = niceNum(niceNum(hi - lo, false) / (target - 1).coerceAtLeast(1), true)
        if (step <= 0.0 || !step.isFinite()) return listOf(lo, hi)
        val start = ceil(lo / step - 1e-9) * step
        val out = ArrayList<Double>()
        var v = start
        var guard = 0
        while (v <= hi + step * 1e-6 && guard < 10_000) {
            out.add(round(v / step) * step)
            v += step
            guard++
        }
        return if (out.isEmpty()) listOf(lo, hi) else out
    }

Format a tick value with just enough decimals for its step, trailing zeros trimmed.

    private fun formatTick(v: Double, step: Double): String {
        val absStep = abs(step)
        val decimals = if (absStep <= 0.0 || !absStep.isFinite()) 0
        else max(0, -floor(log10(absStep)).toInt()).coerceAtMost(4)
        var s = String.format("%.${decimals}f", v)
        if (decimals > 0 && '.' in s) s = s.trimEnd('0').trimEnd('.')
        if (s == "-0") s = "0"
        return s
    }

    private fun numeralWidth(text: String, cell: Double): Double =
        if (text.isEmpty()) 0.0 else text.length * 4.0 * cell - cell

Draw text (digits, -, .) centred at (cx,cy) as a 3×5 dot-matrix of rects — RectSL has no text primitive, so numerals are little rect grids.

    private fun drawNumeral(text: String, cx: Double, cy: Double, cell: Double, color: Color4, refs: MutableList<NodeRef>) {
        if (cell <= 0.0 || text.isEmpty()) return
        val advance = 4.0 * cell
        val totalW = numeralWidth(text, cell)
        val dot = cell * 0.9
        var glyphCx = cx - totalW / 2.0 + 1.5 * cell
        for (ch in text) {
            val pattern = PLOT_GLYPHS[ch] ?: PLOT_GLYPHS[' ']!!
            for (row in 0 until 5) {
                for (col in 0 until 3) {
                    if (pattern[row][col] == '#') {
                        plotRect("glyph", glyphCx + (col - 1) * cell, cy + (2 - row) * cell, dot, dot, color, refs)
                    }
                }
            }
            glyphCx += advance
        }
    }

axes — x/y axis rules + nice-number ticks + numeric labels (rect numerals in the margins) + faint gridlines. Reads the shared domain (defaults to 0..1) and the frame margins. Draw it BEFORE the series so the series paint on top.

source — 49 lines, from line 3385
    private fun drawAxes() {
        val wall = builder().wall
        val frame = plotFrame()
        val (x0, x1) = plotXDomain ?: (0.0 to 1.0)
        val (y0, y1) = plotYDomain ?: (0.0 to 1.0)
        val refs = ArrayList<NodeRef>()
        val axisColor = Color4(0.86, 0.87, 0.92)
        val tickColor = Color4(0.66, 0.67, 0.72)
        val gridColor = Color4(0.80, 0.82, 0.90, 0.10)
        val labelColor = Color4(0.80, 0.81, 0.86)
        val minDim = min(wall.width, wall.height)
        val rule = minDim * 0.005
        val tickLen = minDim * 0.018
        fun mx(v: Double) = frame.left + (v - x0) / (x1 - x0) * frame.width
        fun my(v: Double) = frame.bottom + (v - y0) / (y1 - y0) * frame.height
        val xt = niceTicks(x0, x1)
        val yt = niceTicks(y0, y1)
        val xStep = if (xt.size >= 2) xt[1] - xt[0] else (x1 - x0)
        val yStep = if (yt.size >= 2) yt[1] - yt[0] else (y1 - y0)
        val labelCell = run {
            val avail = listOf(plotMarginB * wall.height, plotMarginL * wall.width).filter { it > 1e-6 }
            if (avail.isEmpty()) 0.0 else (avail.min() * 0.12).coerceAtMost(minDim * 0.028)
        }
        // faint gridlines first, so series drawn afterwards sit on top of them.
        xt.forEach { t -> plotRect("grid-v", mx(t), (frame.bottom + frame.top) / 2.0, rule * 0.6, frame.height, gridColor, refs) }
        yt.forEach { t -> plotRect("grid-h", (frame.left + frame.right) / 2.0, my(t), frame.width, rule * 0.6, gridColor, refs) }
        // axis rules
        plotRect("axis-x", (frame.left + frame.right) / 2.0, frame.bottom, frame.width + rule, rule, axisColor, refs)
        plotRect("axis-y", frame.left, (frame.bottom + frame.top) / 2.0, rule, frame.height + rule, axisColor, refs)
        // x ticks + labels below the axis
        xt.forEach { t ->
            val gx = mx(t)
            plotRect("tick-x", gx, frame.bottom - tickLen / 2.0, rule, tickLen, tickColor, refs)
            if (labelCell > 1e-4) {
                drawNumeral(formatTick(t, xStep), gx, frame.bottom - tickLen - 0.6 * labelCell - 2.5 * labelCell, labelCell, labelColor, refs)
            }
        }
        // y ticks + right-aligned labels left of the axis
        yt.forEach { t ->
            val gy = my(t)
            plotRect("tick-y", frame.left - tickLen / 2.0, gy, tickLen, rule, tickColor, refs)
            if (labelCell > 1e-4) {
                val txt = formatTick(t, yStep)
                val cx = frame.left - tickLen - 0.6 * labelCell - numeralWidth(txt, labelCell) / 2.0
                drawNumeral(txt, cx, gy, labelCell, labelColor, refs)
            }
        }
        push(VSelection(refs))
    }

line/plot — xdata ydata name -- selection. Polyline as rotated thin rects through the shared domain, coloured along colormap name by point order.

    private fun lineSeries() {
        val name = popString()
        val ydata = popData("line"); val xdata = popData("line")
        val stops = Colormaps.stops(name)
        val n = min(xdata.size, ydata.size)
        if (n < 2) throw RectLangException("line: need at least 2 (x, y) points")
        val xv = xdata.take(n); val yv = ydata.take(n)
        val (x0, x1) = resolveXDomain(minMax(xv))
        val (y0, y1) = resolveYDomain(minMax(yv))
        val frame = plotFrame()
        val thickness = min(frame.width, frame.height) * 0.012
        val refs = ArrayList<NodeRef>()
        fun mx(v: Double) = frame.left + (v - x0) / (x1 - x0) * frame.width
        fun my(v: Double) = frame.bottom + (v - y0) / (y1 - y0) * frame.height
        for (i in 0 until n - 1) {
            val ax = mx(xv[i]); val ay = my(yv[i]); val bx = mx(xv[i + 1]); val by = my(yv[i + 1])
            val dx = bx - ax; val dy = by - ay
            val len = hypot(dx, dy)
            val ang = atan2(dy, dx)
            val t = if (n <= 2) 0.5 else i.toDouble() / (n - 2)
            plotRect("line", (ax + bx) / 2.0, (ay + by) / 2.0, len + thickness, thickness, Colormaps.sampleStops(stops, t), refs, ang)
        }
        push(VSelection(refs))
    }

area — xdata ydata name -- selection. Column-filled area from the y=0 baseline to each sample, honouring the shared domain; colour follows normalized y.

    private fun areaSeries() {
        val name = popString()
        val ydata = popData("area"); val xdata = popData("area")
        val stops = Colormaps.stops(name)
        val n = min(xdata.size, ydata.size)
        if (n < 1) throw RectLangException("area: empty data")
        val xv = xdata.take(n); val yv = ydata.take(n)
        val (x0, x1) = resolveXDomain(minMax(xv))
        val ext = minMax(yv)
        val (y0, y1) = resolveYDomain(min(0.0, ext.first) to max(0.0, ext.second))
        val frame = plotFrame()
        fun mx(v: Double) = frame.left + (v - x0) / (x1 - x0) * frame.width
        fun my(v: Double) = frame.bottom + (v - y0) / (y1 - y0) * frame.height
        val base = my(0.0.coerceIn(min(y0, y1), max(y0, y1)))
        val slot = frame.width / n
        val refs = ArrayList<NodeRef>()
        for (i in 0 until n) {
            val top = my(yv[i])
            val h = abs(top - base)
            val t = if (y1 == y0) 0.5 else ((yv[i] - y0) / (y1 - y0)).coerceIn(0.0, 1.0)
            plotRect("area", mx(xv[i]), (top + base) / 2.0, slot * 0.96, h, Colormaps.sampleStops(stops, t), refs)
        }
        push(VSelection(refs))
    }

points — xdata ydata name -- selection. Domain-aware scatter (a shared-domain companion to the classic scatter); colour follows normalized y.

    private fun pointsSeries() {
        val name = popString()
        val ydata = popData("points"); val xdata = popData("points")
        val stops = Colormaps.stops(name)
        val n = min(xdata.size, ydata.size)
        val xv = xdata.take(n); val yv = ydata.take(n)
        val (x0, x1) = resolveXDomain(minMax(xv))
        val (y0, y1) = resolveYDomain(minMax(yv))
        val frame = plotFrame()
        val dot = min(frame.width, frame.height) * 0.028
        val refs = ArrayList<NodeRef>()
        fun mx(v: Double) = frame.left + (v - x0) / (x1 - x0) * frame.width
        fun my(v: Double) = frame.bottom + (v - y0) / (y1 - y0) * frame.height
        for (i in 0 until n) {
            val t = if (y1 == y0) 0.5 else ((yv[i] - y0) / (y1 - y0)).coerceIn(0.0, 1.0)
            plotRect("point", mx(xv[i]), my(yv[i]), dot, dot, Colormaps.sampleStops(stops, t), refs)
        }
        push(VSelection(refs))
    }

colorbar — name -- selection. A continuous colour ramp for name in the right margin, with min/mid/max numerals read from the y domain (drawn when there is room).

    private fun drawColorbar() {
        val name = popString()
        val stops = Colormaps.stops(name)
        val wall = builder().wall
        val frame = plotFrame()
        val minDim = min(wall.width, wall.height)
        val rightMargin = plotMarginR * wall.width
        val barW = if (rightMargin > 1e-6) rightMargin * 0.30 else minDim * 0.03
        val barX = if (rightMargin > 1e-6) frame.right + rightMargin * 0.32 else frame.right - barW
        val steps = 40
        val segH = frame.height / steps
        val refs = ArrayList<NodeRef>()
        for (i in 0 until steps) {
            val t = i.toDouble() / (steps - 1)
            plotRect("cbar", barX, frame.bottom + segH * (i + 0.5), barW, segH * 1.04, Colormaps.sampleStops(stops, t), refs)
        }
        val (y0, y1) = plotYDomain ?: (0.0 to 1.0)
        val yStep = niceTicks(y0, y1).let { if (it.size >= 2) it[1] - it[0] else (y1 - y0) }
        val labels = listOf(0.0 to y0, 0.5 to (y0 + y1) / 2.0, 1.0 to y1)
        // Auto-size so the widest label fits between the bar and the wall's right edge
        // (a 3-char "0.5" is much wider than "0"/"1"), rather than clipping it off-wall.
        val room = wall.width / 2.0 - (barX + barW / 2.0)
        val widestUnits = labels.maxOf { numeralWidth(formatTick(it.second, yStep), 1.0) }
        val labelCell = (room / (1.4 + widestUnits)).coerceIn(0.0, minDim * 0.024)
        if (labelCell > 1e-4) {
            val labelColor = Color4(0.80, 0.81, 0.86)
            labels.forEach { (frac, v) ->
                val txt = formatTick(v, yStep)
                val cx = barX + barW / 2.0 + labelCell + numeralWidth(txt, labelCell) / 2.0
                drawNumeral(txt, cx, frame.bottom + frac * frame.height, labelCell, labelColor, refs)
            }
        }
        push(VSelection(refs))
    }

legend — count name -- selection. A key of count discrete colour swatches sampling colormap name, stacked in the top-right of the plot area.

    private fun drawLegend() {
        val name = popString()
        val count = popNum().toInt().coerceAtLeast(1)
        val stops = Colormaps.stops(name)
        val wall = builder().wall
        val frame = plotFrame()
        val minDim = min(wall.width, wall.height)
        val sw = minDim * 0.05
        val gap = sw * 0.35
        val x = frame.right - sw * 0.7
        val topY = frame.top - sw * 0.7
        val refs = ArrayList<NodeRef>()
        for (i in 0 until count) {
            val t = if (count == 1) 0.5 else i.toDouble() / (count - 1)
            plotRect("legend", x, topY - i * (sw + gap), sw, sw, Colormaps.sampleStops(stops, t), refs)
        }
        push(VSelection(refs))
    }

internal fun word

internal, not private, so RectSLPattern.kt can register the musical words through the same one registration path rather than a parallel mechanism.

    internal fun word(
        spelling: String,
        effect: RectWordEffect = RectWordEffect.Ordinary,
        apply: (RectLangVm) -> Unit
    ) {
        words[spelling] = Word(RectWordMetadata(spelling, effect), apply)
    }

internal fun mutator(name: String, apply: (RectLangVm) -> Unit) {

    internal fun mutator(name: String, apply: (RectLangVm) -> Unit) {
        require(!name.startsWith("->")) { "Mutator names are registered without their -> prefix: $name" }
        word("->$name", RectWordEffect.Mutator, apply)
    }

enum class TrackMode { Tcb, Linear, Hold }

fun quoteToScalarTrack(quote: VQuote, mode: TrackMode): Track<Double>

A track quote — '( 0=0 2=4 ) — as keys.

A key value may be followed by free-param <name>, which is the same spelling and the same meaning as the standalone word: the key plays at the number written, and says it would rather be solved for. '( 0=0 2=4 free-param peak ) is a ramp whose top is the unknown.

body — 43 lines, from line 3593
    fun quoteToScalarTrack(quote: VQuote, mode: TrackMode): Track<Double> {
        // `time=value` reads more clearly than a bare space between the two, so the
        // `=` separator is accepted (and ignored) between the numbers of each pair.
        val tokens = quote.tokens.filterNot { it is Token.Word && it.name == "=" }
        val keys = ArrayList<TcbKey>()
        var i = 0
        while (i < tokens.size) {
            val time = tokens[i] as? Token.Number
                ?: throw RectLangException("Track quote expected only numbers, found ${tokens[i]}")
            // Both spellings of the old refusal are kept where they applied: a dangling time is an
            // unpaired key, anything else in a value slot is not a number.
            val value = when (val v = tokens.getOrNull(i + 1)) {
                is Token.Number -> v
                null -> throw RectLangException("Track quote expected time/value pairs")
                else -> throw RectLangException("Track quote expected only numbers, found $v")
            }
            i += 2
            val marker = tokens.getOrNull(i)
            keys += if (marker is Token.Word && marker.name == "free-param") {
                // Word or string, the same two spellings [consumeName] takes for the standalone word.
                val named = when (val n = tokens.getOrNull(i + 1)) {
                    is Token.Word -> n.name
                    is Token.StringLit -> n.value
                    else -> throw RectLangException(
                        "free-param in a track quote expected a name after it"
                    )
                }
                i += 2
                TcbKey(time.value, FreeParamTrack(named, value.value))
            } else {
                TcbKey(time.value, value.value)
            }
        }
        if (keys.isEmpty()) {
            throw RectLangException("Track quote expected time/value pairs")
        }
        return when (mode) {
            TrackMode.Tcb -> TcbTrack(keys)
            TrackMode.Linear -> LinearScalarTrack(keys)
            TrackMode.Hold -> HoldScalarTrack(keys)
        }
    }
}

fun evalRectSLStack(source: String): List<Value>

Evaluate a RectSL program and return its residual stack — for tooling and tests that need the values a program leaves behind (e.g. a VFn distribution curve built through the Functions bridge, to inspect or evolve its graph).

fun evalRectSLStack(source: String): List<Value> {
    val vm = RectLangVm()
    vm.eval(RectLangTokenizer(source).tokenize())
    return vm.stackValues()
}

sealed interface NodeRef

val id: String

data class Rect(override val id: String) : NodeRef

data class Group(override val id: String) : NodeRef

internal data class PlotFrame(val left: Double, val right: Double, val bottom: Double, val top: Double)

World-space rectangle of a RectSL plotting area (wall centred at origin, +y up).

val width: Double get() = right - left

val height: Double get() = top - bottom

internal val PLOT_GLYPHS: Map<Char, List<String>>

A 3×5 dot-matrix font. RectSL has no text primitive (everything is a rect), so a label is drawn as a little grid of rects — a pure builder, not a renderer feature. Rows run top→bottom.

The numerals are what axes/colorbar tick labels need. The uppercase letters are for captions on a raster figure (RectSLFitFigure) that has no text primitive either, and they are here rather than in a second table because there should be one dot-matrix font in this package, not two.

body — 116 lines, from line 3670
internal val PLOT_GLYPHS: Map<Char, List<String>> = mapOf(
    '0' to listOf("###", "#.#", "#.#", "#.#", "###"),
    '1' to listOf(".#.", "##.", ".#.", ".#.", "###"),
    '2' to listOf("###", "..#", "###", "#..", "###"),
    '3' to listOf("###", "..#", "###", "..#", "###"),
    '4' to listOf("#.#", "#.#", "###", "..#", "..#"),
    '5' to listOf("###", "#..", "###", "..#", "###"),
    '6' to listOf("###", "#..", "###", "#.#", "###"),
    '7' to listOf("###", "..#", "..#", "..#", "..#"),
    '8' to listOf("###", "#.#", "###", "#.#", "###"),
    '9' to listOf("###", "#.#", "###", "..#", "###"),
    '-' to listOf("...", "...", "###", "...", "..."),
    '.' to listOf("...", "...", "...", "...", ".#."),
    ' ' to listOf("...", "...", "...", "...", "..."),
    'A' to listOf("###", "#.#", "###", "#.#", "#.#"),
    'B' to listOf("##.", "#.#", "##.", "#.#", "##."),
    'C' to listOf("###", "#..", "#..", "#..", "###"),
    'D' to listOf("##.", "#.#", "#.#", "#.#", "##."),
    'E' to listOf("###", "#..", "##.", "#..", "###"),
    'F' to listOf("###", "#..", "##.", "#..", "#.."),
    'G' to listOf("###", "#..", "#.#", "#.#", "###"),
    'H' to listOf("#.#", "#.#", "###", "#.#", "#.#"),
    'I' to listOf("###", ".#.", ".#.", ".#.", "###"),
    'J' to listOf("..#", "..#", "..#", "#.#", "###"),
    'K' to listOf("#.#", "#.#", "##.", "#.#", "#.#"),
    'L' to listOf("#..", "#..", "#..", "#..", "###"),
    'M' to listOf("#.#", "###", "###", "#.#", "#.#"),
    'N' to listOf("##.", "#.#", "#.#", "#.#", "#.#"),
    'O' to listOf("###", "#.#", "#.#", "#.#", "###"),
    'P' to listOf("###", "#.#", "###", "#..", "#.."),
    'Q' to listOf("###", "#.#", "###", "..#", "..#"),
    'R' to listOf("###", "#.#", "##.", "#.#", "#.#"),
    'S' to listOf("###", "#..", "###", "..#", "###"),
    'T' to listOf("###", ".#.", ".#.", ".#.", ".#."),
    'U' to listOf("#.#", "#.#", "#.#", "#.#", "###"),
    'V' to listOf("#.#", "#.#", "#.#", "#.#", ".#."),
    'W' to listOf("#.#", "#.#", "###", "###", "#.#"),
    'X' to listOf("#.#", "#.#", ".#.", "#.#", "#.#"),
    'Y' to listOf("#.#", "#.#", ".#.", ".#.", ".#."),
    'Z' to listOf("###", "..#", ".#.", "#..", "###"),
    '=' to listOf("...", "###", "...", "###", "..."),
    ':' to listOf("...", ".#.", "...", ".#.", "..."),
    '(' to listOf(".#.", "#..", "#..", "#..", ".#."),
    ')' to listOf(".#.", "..#", "..#", "..#", ".#."),
    '/' to listOf("..#", "..#", ".#.", "#..", "#..")
)

private fun NodeRef.toValue(): Value = when (this) {
    is NodeRef.Rect -> VRect(id)
    is NodeRef.Group -> VGroup(id)
}

private sealed class NodeDraft {
    abstract var id: String
    abstract val tags: MutableSet<String>
}

private class RectDraft(
    override var id: String
) : NodeDraft() {
    var x: Track<Double> = constantTrack(0.0)
    var y: Track<Double> = constantTrack(0.0)
    var w: Track<Double> = constantTrack(1.0)
    var h: Track<Double> = constantTrack(1.0)
    var rotation: Track<Double> = constantTrack(0.0)
    var scaleX: Track<Double> = constantTrack(1.0)
    var scaleY: Track<Double> = constantTrack(1.0)
    var alpha: Track<Double> = constantTrack(1.0)
    var z: Track<Double> = constantTrack(0.0)
    var fill: Track<PaintSpec> = constantPaintTrack(PaintSpec.Solid(Color4.WHITE))
    var material: MaterialBinding = MaterialBinding(0)
    override val tags: MutableSet<String> = linkedSetOf()

    fun freeze(): RectNode = RectNode(id, x, y, w, h, rotation, scaleX, scaleY, alpha, z, fill, material, tags.toSet())
    fun cloneAs(newId: String): RectDraft =
        RectDraft(newId).also {
            it.x = x
            it.y = y
            it.w = w
            it.h = h
            it.rotation = rotation
            it.scaleX = scaleX
            it.scaleY = scaleY
            it.alpha = alpha
            it.z = z
            it.fill = fill
            it.material = material
            it.tags += tags
        }
}

private class GroupDraft(
    override var id: String
) : NodeDraft() {
    val children: MutableList<String> = ArrayList()
    var x: Track<Double> = constantTrack(0.0)
    var y: Track<Double> = constantTrack(0.0)
    var rotation: Track<Double> = constantTrack(0.0)
    var scaleX: Track<Double> = constantTrack(1.0)
    var scaleY: Track<Double> = constantTrack(1.0)
    var alpha: Track<Double> = constantTrack(1.0)
    override val tags: MutableSet<String> = linkedSetOf()

    fun freeze(): GroupNode = GroupNode(id, children.toList(), x, y, rotation, scaleX, scaleY, alpha, tags.toSet())
    fun cloneShallowAs(newId: String): GroupDraft =
        GroupDraft(newId).also {
            it.children += children
            it.x = x
            it.y = y
            it.rotation = rotation
            it.scaleX = scaleX
            it.scaleY = scaleY
            it.alpha = alpha
            it.tags += tags
        }
}

class SceneBuilder

var wall: WallSpec = WallSpec(30.0, 4.0, UnitKind.Meters)

var duration: Double = 10.0

var fps: Double = 60.0

var cps: Double = 1.0

Cycles per second — the scene's musical clock, and the one number that converts between the pattern language's cycles and RectSL's seconds. The default 1.0 is Tidal's own, and makes a scene that never says ->tempo behave as if a cycle were a second. Nothing outside the musical words reads it.

var beatsPerCycle: Double = 4.0

Beats per cycle — the denominator ->tempo divides a bpm by. Four is a 4/4 bar.

var bpm: Double? = null

The last bpm ->tempo was given, remembered so that ->tempo and ->beats-per-cycle commute. Null when cps was set directly, which is not a tempo divided by anything.

var fit: FitPolicy = FitPolicy.Contain

var background: Color4 = Color4.BLACK

    private val nodes = linkedMapOf<String, NodeDraft>()
    private val active = ArrayDeque<NodeRef>()

internal var onNode: ((id: String, renamedFrom: String?) -> Unit)? = null

Told each node id as it enters the scene — created, cloned, or renamed into (with the id it had) — so a caller can say which line of the source made each rectangle. See compileRectSLWithSource.

    private val groupStack = ArrayDeque<String>()
    private val materials = linkedMapOf(
        "plain" to MutableMaterialSpec("plain", 0, null),
        "soft-edge" to MutableMaterialSpec("soft-edge", 1, null),
        "diagnostic" to MutableMaterialSpec("diagnostic", 2, null)
    )
    private val layers = linkedMapOf<String, Double>()
    private var nextNodeId = 1
    private var nextMaterialId = 3
    private var nextLayer = 0.0
    private var backLayer = -1.0

fun build(): SceneProgram

    fun build(): SceneProgram {
        val table = MaterialTable(
            modules = materials.values.mapNotNull { it.moduleSpec() },
            params = FloatArray(0)
        )
        return SceneProgram(
            wall = wall,
            duration = duration,
            fps = fps,
            nodes = nodes.values.map {
                when (it) {
                    is RectDraft -> it.freeze()
                    is GroupDraft -> it.freeze()
                }
            },
            fit = fit,
            background = background,
            materialTable = table
        )
    }

fun hasMaterial(name: String): Boolean = name in materials

fun createRect(id: String?): VRect

    fun createRect(id: String?): VRect {
        val rid = uniqueId(id ?: "rect-${nextNodeId++}")
        val draft = RectDraft(rid)
        nodes[rid] = draft
        onNode?.invoke(rid, null)
        groupStack.lastOrNull()?.let { parent -> group(parent).children += rid }
        active.addLast(NodeRef.Rect(rid))
        return VRect(rid)
    }

fun createGroup(id: String?): VGroup

    fun createGroup(id: String?): VGroup {
        val gid = uniqueId(id ?: "group-${nextNodeId++}")
        val draft = GroupDraft(gid)
        nodes[gid] = draft
        onNode?.invoke(gid, null)
        groupStack.lastOrNull()?.let { parent -> group(parent).children += gid }
        groupStack.addLast(gid)
        active.addLast(NodeRef.Group(gid))
        return VGroup(gid)
    }

fun groupSelected(id: String?, refs: List<NodeRef>): VGroup

    fun groupSelected(id: String?, refs: List<NodeRef>): VGroup {
        if (refs.isEmpty()) throw RectLangException("group-selected needs at least one rect or group")
        refs.forEach { requireNode(it.id) }
        val selectedIds = refs.map { it.id }.toSet()
        val gid = uniqueId(id ?: "group-${nextNodeId++}")
        val draft = GroupDraft(gid)
        val parent = commonParentOf(selectedIds)
        val parentInsertAt = parent?.let { parentId ->
            group(parentId).children.indexOfFirst { it in selectedIds }.takeIf { it >= 0 }
        }
        nodes[gid] = draft
        onNode?.invoke(gid, null)

        nodes.values.filterIsInstance<GroupDraft>().forEach { group ->
            group.children.removeAll(selectedIds)
        }
        draft.children += refs.map { it.id }

        if (parent != null && parent !in selectedIds) {
            val children = group(parent).children
            val insertAt = parentInsertAt ?: children.size
            children.add(insertAt, gid)
        } else {
            groupStack.lastOrNull()?.takeIf { it !in selectedIds }?.let { group(it).children += gid }
        }

        active.addLast(NodeRef.Group(gid))
        return VGroup(gid)
    }

fun cloneNode(ref: NodeRef): Value

    fun cloneNode(ref: NodeRef): Value {
        val cloned = cloneNodeRef(ref)
        active.addLast(cloned)
        return cloned.toValue()
    }

fun cloneNodeDeep(ref: NodeRef): Value

    fun cloneNodeDeep(ref: NodeRef): Value {
        val cloned = cloneNodeDeepRef(ref)
        active.addLast(cloned)
        return cloned.toValue()
    }

fun cloneSelection(refs: List<NodeRef>, deep: Boolean): Value

    fun cloneSelection(refs: List<NodeRef>, deep: Boolean): Value {
        val cloned = refs.map { if (deep) cloneNodeDeepRef(it) else cloneNodeRef(it) }
        cloned.lastOrNull()?.let { active.addLast(it) }
        return VSelection(cloned)
    }

fun rename(ref: NodeRef, newId: String): Value

    fun rename(ref: NodeRef, newId: String): Value {
        val oldId = ref.id
        if (oldId == newId) return ref.toValue()
        val node = nodes.remove(oldId) ?: throw RectLangException("Unknown node '$oldId'")
        val fresh = uniqueId(newId)
        node.id = fresh
        nodes[fresh] = node
        onNode?.invoke(fresh, oldId)
        nodes.values.filterIsInstance<GroupDraft>().forEach { group ->
            group.children.replaceAll { if (it == oldId) fresh else it }
        }
        replaceActive(oldId, fresh, node)
        for (i in groupStack.indices) {
            if (groupStack[i] == oldId) groupStack[i] = fresh
        }
        return if (node is RectDraft) VRect(fresh) else VGroup(fresh)
    }

fun select(ref: NodeRef)

    fun select(ref: NodeRef) {
        requireNode(ref.id)
        active.addLast(ref)
    }

fun endScope()

    fun endScope() {
        if (active.isNotEmpty()) active.removeLast()
    }

fun endGroup()

    fun endGroup() {
        val groupId = if (groupStack.isEmpty()) throw RectLangException("No open group to end") else groupStack.removeLast()
        if (active.lastOrNull()?.id == groupId) active.removeLast()
    }

fun activeRef(): NodeRef? = active.lastOrNull()

fun withActive(ref: NodeRef, block: () -> Unit) {

    fun withActive(ref: NodeRef, block: () -> Unit) {
        requireNode(ref.id)
        active.addLast(ref)
        try {
            block()
        } finally {
            if (active.lastOrNull() == ref) {
                active.removeLast()
            } else {
                active.remove(ref)
            }
        }
    }

fun refFor(id: String): NodeRef

    fun refFor(id: String): NodeRef {
        val node = nodes[id] ?: throw RectLangException("Unknown node '$id'")
        return if (node is RectDraft) NodeRef.Rect(id) else NodeRef.Group(id)
    }

fun allRefs(): List<NodeRef>

    fun allRefs(): List<NodeRef> =
        nodes.values.map { it.toRef() }

fun rootRefs(): List<NodeRef>

    fun rootRefs(): List<NodeRef> {
        val childIds = nodes.values.filterIsInstance<GroupDraft>().flatMap { it.children }.toSet()
        return nodes.values.filter { it.id !in childIds }.map { it.toRef() }
    }

fun childrenOf(refs: List<NodeRef>): List<NodeRef>

    fun childrenOf(refs: List<NodeRef>): List<NodeRef> =
        refs.flatMap { ref ->
            when (val node = requireNode(ref.id)) {
                is RectDraft -> emptyList()
                is GroupDraft -> node.children.map { refFor(it) }
            }
        }.distinctBy { it.id }

fun descendantsOf(refs: List<NodeRef>): List<NodeRef>

    fun descendantsOf(refs: List<NodeRef>): List<NodeRef> {
        val out = ArrayList<NodeRef>()
        val seen = linkedSetOf<String>()
        fun visit(ref: NodeRef) {
            when (val node = requireNode(ref.id)) {
                is RectDraft -> Unit
                is GroupDraft -> node.children.forEach { childId ->
                    if (seen.add(childId)) {
                        val child = refFor(childId)
                        out += child
                        visit(child)
                    }
                }
            }
        }
        refs.forEach(::visit)
        return out
    }

fun tag(tag: String)

    fun tag(tag: String) {
        target().tags += tag
    }

fun hasTag(tag: String): Boolean = target().tags.contains(tag)

fun hasTag(ref: NodeRef, tag: String): Boolean

    fun hasTag(ref: NodeRef, tag: String): Boolean =
        requireNode(ref.id).tags.contains(tag)

fun scalarTrack(name: String): Track<Double>

    fun scalarTrack(name: String): Track<Double> =
        when (val t = target()) {
            is RectDraft -> when (name) {
                "x" -> t.x
                "y" -> t.y
                "w" -> t.w
                "h" -> t.h
                "rot", "rotation" -> t.rotation
                "scale-x" -> t.scaleX
                "scale-y" -> t.scaleY
                "alpha" -> t.alpha
                "z", "layer" -> t.z
                else -> throw RectLangException("Unknown rect scalar track '$name'")
            }
            is GroupDraft -> when (name) {
                "x" -> t.x
                "y" -> t.y
                "rot", "rotation" -> t.rotation
                "scale-x" -> t.scaleX
                "scale-y" -> t.scaleY
                "alpha" -> t.alpha
                "w", "h", "z", "layer" -> throw RectLangException("Groups do not have '$name' tracks")
                else -> throw RectLangException("Unknown group scalar track '$name'")
            }
        }

fun setX(track: Track<Double>)

    fun setX(track: Track<Double>) {
        when (val t = target()) {
            is RectDraft -> t.x = track
            is GroupDraft -> t.x = track
        }
    }

fun setY(track: Track<Double>)

    fun setY(track: Track<Double>) {
        when (val t = target()) {
            is RectDraft -> t.y = track
            is GroupDraft -> t.y = track
        }
    }

fun setW(track: Track<Double>)

    fun setW(track: Track<Double>) {
        rectTarget().w = track
    }

fun setH(track: Track<Double>)

    fun setH(track: Track<Double>) {
        rectTarget().h = track
    }

fun setRotation(track: Track<Double>)

    fun setRotation(track: Track<Double>) {
        when (val t = target()) {
            is RectDraft -> t.rotation = track
            is GroupDraft -> t.rotation = track
        }
    }

fun addRotation(track: Track<Double>)

    fun addRotation(track: Track<Double>) {
        when (val t = target()) {
            is RectDraft -> t.rotation = t.rotation + track
            is GroupDraft -> t.rotation = t.rotation + track
        }
    }

fun faceTravel(offset: Double)

Set rotation to the heading of the active object's own x/y tracks (+offset).

    fun faceTravel(offset: Double) {
        when (val t = target()) {
            is RectDraft -> t.rotation = HeadingTrack(t.x, t.y, offset = offset)
            is GroupDraft -> t.rotation = HeadingTrack(t.x, t.y, offset = offset)
        }
    }

fun gateVisibility(lo: Double, hi: Double) = multiplyAlpha(GateTrack(lo, hi))

Gate the active object's alpha so it is only visible while lo <= t < hi.

fun live(birth: Double, life: Double)

The emitter primitive: play the active object's animation from its birth (local time 0 becomes birth, like delay-all-time) and make it visible only during [birth, birth + life]. Authoring a trajectory in local time and firing it at birth is how one template becomes a staggered stream of bullets.

    fun live(birth: Double, life: Double) {
        mapActiveScalarTracks { TimeWarpTrack(it, TimeWarpKind.SHIFT, birth) }
        multiplyAlpha(GateTrack(birth, birth + life))
    }

fun setScaleX(track: Track<Double>)

    fun setScaleX(track: Track<Double>) {
        when (val t = target()) {
            is RectDraft -> t.scaleX = track
            is GroupDraft -> t.scaleX = track
        }
    }

fun setScaleY(track: Track<Double>)

    fun setScaleY(track: Track<Double>) {
        when (val t = target()) {
            is RectDraft -> t.scaleY = track
            is GroupDraft -> t.scaleY = track
        }
    }

fun setAlpha(track: Track<Double>)

    fun setAlpha(track: Track<Double>) {
        when (val t = target()) {
            is RectDraft -> t.alpha = track
            is GroupDraft -> t.alpha = track
        }
    }

fun setZ(track: Track<Double>)

    fun setZ(track: Track<Double>) {
        when (val t = target()) {
            is RectDraft -> t.z = track
            is GroupDraft -> descendantsOf(listOf(NodeRef.Group(t.id))).forEach { ref ->
                (requireNode(ref.id) as? RectDraft)?.z = track
            }
        }
    }

fun setLayer(name: String)

    fun setLayer(name: String) {
        setZ(constantTrack(layerValue(name)))
    }

fun moveFront()

    fun moveFront() {
        nextLayer += 1.0
        setZ(constantTrack(nextLayer))
    }

fun moveBack()

    fun moveBack() {
        setZ(constantTrack(backLayer))
        backLayer -= 1.0
    }

fun multiplyAlpha(track: Track<Double>)

    fun multiplyAlpha(track: Track<Double>) {
        when (val t = target()) {
            is RectDraft -> t.alpha = t.alpha * track
            is GroupDraft -> t.alpha = t.alpha * track
        }
    }

fun setFill(track: Track<PaintSpec>)

    fun setFill(track: Track<PaintSpec>) {
        rectTarget().fill = track
    }

fun setMaterial(name: String)

    fun setMaterial(name: String) {
        val spec = materials[name] ?: throw RectLangException("Unknown material '$name'")
        val rect = rectTarget()
        rect.material = rect.material.copy(materialId = spec.materialId, name = name)
    }

fun setShaderData(data: Vec4f)

    fun setShaderData(data: Vec4f) {
        val rect = rectTarget()
        rect.material = rect.material.copy(shaderData = data)
    }

fun defineStackSLMaterial(name: String, source: String)

    fun defineStackSLMaterial(name: String, source: String) {
        val id = materials[name]?.materialId ?: nextMaterialId++
        materials[name] = MutableMaterialSpec(name, id, source)
    }

fun mirrorX(axis: Double)

    fun mirrorX(axis: Double) {
        when (val t = target()) {
            is RectDraft -> {
                t.x = reflectAbout(t.x, axis)
                t.rotation = MappedDoubleTrack(t.rotation, TrackOp1.NEG)
            }
            is GroupDraft -> {
                t.x = reflectAbout(t.x, axis)
                t.rotation = MappedDoubleTrack(t.rotation, TrackOp1.NEG)
            }
        }
    }

fun mirrorY(axis: Double)

    fun mirrorY(axis: Double) {
        when (val t = target()) {
            is RectDraft -> {
                t.y = reflectAbout(t.y, axis)
                t.rotation = MappedDoubleTrack(t.rotation, TrackOp1.NEG)
            }
            is GroupDraft -> {
                t.y = reflectAbout(t.y, axis)
                t.rotation = MappedDoubleTrack(t.rotation, TrackOp1.NEG)
            }
        }
    }

fun translate(dx: Track<Double>, dy: Track<Double>)

    fun translate(dx: Track<Double>, dy: Track<Double>) {
        when (val t = target()) {
            is RectDraft -> {
                t.x = t.x + dx
                t.y = t.y + dy
            }
            is GroupDraft -> {
                t.x = t.x + dx
                t.y = t.y + dy
            }
        }
    }

fun mapActiveScalarTracks(transform: (Track<Double>) -> Track<Double>) {

    fun mapActiveScalarTracks(transform: (Track<Double>) -> Track<Double>) {
        mapScalarTracks(activeRef() ?: throw RectLangException("No active rect or group"), transform)
    }

fun staggerTime(refs: List<NodeRef>, step: Double)

    fun staggerTime(refs: List<NodeRef>, step: Double) {
        refs.forEachIndexed { index, ref ->
            val delay = step * index
            mapScalarTracks(ref) { TimeWarpTrack(it, TimeWarpKind.SHIFT, delay) }
        }
    }

fun instantiateArray(template: NodeRef, count: Int): List<NodeRef>

Element 0 is the template itself; the remaining count-1 are fresh clones (deep for groups).

    fun instantiateArray(template: NodeRef, count: Int): List<NodeRef> {
        requireNode(template.id)
        val refs = ArrayList<NodeRef>(count)
        refs += template
        repeat(count - 1) {
            refs += when (requireNode(template.id)) {
                is GroupDraft -> cloneNodeDeepRef(template)
                is RectDraft -> cloneNodeRef(template)
            }
        }
        return refs
    }

fun captureNewNodes(block: () -> Unit): List<NodeRef> {

Run block, then return the nodes it created, in creation order.

    fun captureNewNodes(block: () -> Unit): List<NodeRef> {
        val before = HashSet(nodes.keys)
        block()
        return nodes.keys.filter { it !in before }.map { refFor(it) }
    }

fun stagger(refs: List<NodeRef>, dx: Double, dy: Double)

Increasing positional offset over selection order: element k moves by k*(dx,dy).

    fun stagger(refs: List<NodeRef>, dx: Double, dy: Double) {
        refs.forEachIndexed { index, ref ->
            val ox = dx * index
            val oy = dy * index
            when (val node = requireNode(ref.id)) {
                is RectDraft -> {
                    node.x = node.x + constantTrack(ox)
                    node.y = node.y + constantTrack(oy)
                }
                is GroupDraft -> {
                    node.x = node.x + constantTrack(ox)
                    node.y = node.y + constantTrack(oy)
                }
            }
        }
    }

fun positionAt(ref: NodeRef, t: Double): Vec2d

Sample a node's own position tracks at time t (the split point for split).

    fun positionAt(ref: NodeRef, t: Double): Vec2d = when (val n = requireNode(ref.id)) {
        is RectDraft -> Vec2d(n.x.sample(t), n.y.sample(t))
        is GroupDraft -> Vec2d(n.x.sample(t), n.y.sample(t))
    }

fun cloneShardAtOrigin(parent: NodeRef): NodeRef

Clone parent (deep for groups) and move the clone's origin to (0, 0) so a shard's flight can be authored around the origin, then translated onto the split point. Every other channel (size, fill, scale, rotation) is inherited from the parent.

    fun cloneShardAtOrigin(parent: NodeRef): NodeRef {
        val ref = when (requireNode(parent.id)) {
            is GroupDraft -> cloneNodeDeepRef(parent)
            is RectDraft -> cloneNodeRef(parent)
        }
        when (val n = requireNode(ref.id)) {
            is RectDraft -> { n.x = constantTrack(0.0); n.y = constantTrack(0.0) }
            is GroupDraft -> { n.x = constantTrack(0.0); n.y = constantTrack(0.0) }
        }
        return ref
    }

fun setPolar(angle: Double, radius: Double)

Place the active object on a circle of the given radius at the given angle (radians).

    fun setPolar(angle: Double, radius: Double) {
        setX(constantTrack(radius * cos(angle)))
        setY(constantTrack(radius * sin(angle)))
    }

internal fun combineScalarTracks

Snapshot a node's scalar tracks, run body (which may rewrite any of them), then replace each with combine(before, after).

This is what makes a conditional transform expressible at all. RectSL has no way to say "do this only sometimes" — a mutator rewrites a track and the old one is gone. Holding both versions and choosing between them per sample turns any existing mutator into a variation operator, which is how ->every works without every mutator knowing about cycles.

    internal fun combineScalarTracks(
        ref: NodeRef,
        body: () -> Unit,
        combine: (before: Track<Double>, after: Track<Double>) -> Track<Double>
    ) {
        val node = requireNode(ref.id)
        val before: List<Track<Double>> = when (node) {
            is RectDraft -> listOf(node.x, node.y, node.w, node.h, node.rotation, node.scaleX, node.scaleY, node.alpha, node.z)
            is GroupDraft -> listOf(node.x, node.y, node.rotation, node.scaleX, node.scaleY, node.alpha)
        }
        body()
        when (val after = requireNode(ref.id)) {
            is RectDraft -> {
                after.x = combine(before[0], after.x)
                after.y = combine(before[1], after.y)
                after.w = combine(before[2], after.w)
                after.h = combine(before[3], after.h)
                after.rotation = combine(before[4], after.rotation)
                after.scaleX = combine(before[5], after.scaleX)
                after.scaleY = combine(before[6], after.scaleY)
                after.alpha = combine(before[7], after.alpha)
                after.z = combine(before[8], after.z)
            }
            is GroupDraft -> {
                after.x = combine(before[0], after.x)
                after.y = combine(before[1], after.y)
                after.rotation = combine(before[2], after.rotation)
                after.scaleX = combine(before[3], after.scaleX)
                after.scaleY = combine(before[4], after.scaleY)
                after.alpha = combine(before[5], after.alpha)
            }
        }
    }

internal fun mapScalarTracks(ref: NodeRef, transform: (Track<Double>) -> Track<Double>) {

body — 124 lines, from line 4336
    internal fun mapScalarTracks(ref: NodeRef, transform: (Track<Double>) -> Track<Double>) {
        when (val node = requireNode(ref.id)) {
            is RectDraft -> {
                node.x = transform(node.x)
                node.y = transform(node.y)
                node.w = transform(node.w)
                node.h = transform(node.h)
                node.rotation = transform(node.rotation)
                node.scaleX = transform(node.scaleX)
                node.scaleY = transform(node.scaleY)
                node.alpha = transform(node.alpha)
                node.z = transform(node.z)
            }
            is GroupDraft -> {
                node.x = transform(node.x)
                node.y = transform(node.y)
                node.rotation = transform(node.rotation)
                node.scaleX = transform(node.scaleX)
                node.scaleY = transform(node.scaleY)
                node.alpha = transform(node.alpha)
            }
        }
    }

    private fun cloneNodeRef(ref: NodeRef): NodeRef {
        val node = requireNode(ref.id)
        val id = uniqueId("${ref.id}-copy")
        return when (node) {
            is RectDraft -> {
                nodes[id] = node.cloneAs(id)
                onNode?.invoke(id, null)
                NodeRef.Rect(id)
            }
            is GroupDraft -> {
                nodes[id] = node.cloneShallowAs(id)
                onNode?.invoke(id, null)
                NodeRef.Group(id)
            }
        }
    }

    private fun cloneNodeDeepRef(ref: NodeRef): NodeRef {
        val node = requireNode(ref.id)
        val id = uniqueId("${ref.id}-copy")
        return when (node) {
            is RectDraft -> {
                nodes[id] = node.cloneAs(id)
                onNode?.invoke(id, null)
                NodeRef.Rect(id)
            }
            is GroupDraft -> {
                val clone = node.cloneShallowAs(id)
                clone.children.clear()
                nodes[id] = clone
                onNode?.invoke(id, null)
                node.children.forEach { childId ->
                    clone.children += cloneNodeDeepRef(refFor(childId)).id
                }
                NodeRef.Group(id)
            }
        }
    }

    private fun uniqueId(requested: String): String {
        if (requested !in nodes) return requested
        var n = 2
        while ("$requested-$n" in nodes) n++
        return "$requested-$n"
    }

    private fun requireNode(id: String): NodeDraft =
        nodes[id] ?: throw RectLangException("Unknown node '$id'")

    private fun target(): NodeDraft {
        val ref = active.lastOrNull() ?: throw RectLangException("No active rect or group")
        return requireNode(ref.id)
    }

    private fun rectTarget(): RectDraft =
        target() as? RectDraft ?: throw RectLangException("Current target is not a rect")

    private fun group(id: String): GroupDraft =
        nodes[id] as? GroupDraft ?: throw RectLangException("Unknown group '$id'")

    private fun commonParentOf(ids: Set<String>): String? {
        if (ids.isEmpty()) return null
        return nodes.values.filterIsInstance<GroupDraft>().firstOrNull { group ->
            ids.all { it in group.children }
        }?.id
    }

    private fun layerValue(name: String): Double =
        layers.getOrPut(name) { nextLayer++ }

    private fun replaceActive(oldId: String, fresh: String, node: NodeDraft) {
        for (index in active.indices) {
            if (active[index].id == oldId) {
                active[index] = if (node is RectDraft) NodeRef.Rect(fresh) else NodeRef.Group(fresh)
            }
        }
    }

    private fun NodeDraft.toRef(): NodeRef =
        if (this is RectDraft) NodeRef.Rect(id) else NodeRef.Group(id)
}

private data class MutableMaterialSpec(
    val name: String,
    val materialId: Int,
    val stackSLSource: String?
) {
    fun moduleSpec(): ShaderModuleSpec? =
        stackSLSource?.let {
            ShaderModuleSpec(
                name = name,
                materialId = materialId,
                source = it,
                prefix = "rectmat_${sanitizeGlslIdentifier(name)}"
            )
        }
}

private operator fun Track<Double>.plus(other: Track<Double>): Track<Double> = BinaryDoubleTrack(this, other, TrackOp2.ADD)
private operator fun Track<Double>.times(other: Track<Double>): Track<Double> = BinaryDoubleTrack(this, other, TrackOp2.MUL)

2 * axis - track — a reflection of a coordinate track about a fixed axis.

private fun reflectAbout(track: Track<Double>, axis: Double): Track<Double> =
    BinaryDoubleTrack(constantTrack(2.0 * axis), track, TrackOp2.SUB)

fun rectCornersWorld(r: SampledRect): Array<Vec2d>

fun rectCornersWorld(r: SampledRect): Array<Vec2d> {
    val hw = 0.5 * r.size.x * r.scale.x
    val hh = 0.5 * r.size.y * r.scale.y
    val local = arrayOf(
        Vec2d(-hw, -hh),
        Vec2d(hw, -hh),
        Vec2d(hw, hh),
        Vec2d(-hw, hh)
    )
    val c = cos(r.rotation)
    val s = sin(r.rotation)
    return Array(4) { i ->
        val p = local[i]
        Vec2d(
            r.center.x + c * p.x - s * p.y,
            r.center.y + s * p.x + c * p.y
        )
    }
}

private fun rectCornersLocal(r: SampledRect): Array<Vec2d> {
    val hw = 0.5 * r.size.x * r.scale.x
    val hh = 0.5 * r.size.y * r.scale.y
    return arrayOf(
        Vec2d(-hw, -hh),
        Vec2d(hw, -hh),
        Vec2d(hw, hh),
        Vec2d(-hw, hh)
    )
}

fun lowerSampledScene(scene: SampledScene): RectMesh

fun lowerSampledScene(scene: SampledScene): RectMesh {
    val vertices = ArrayList<RectVertex>(scene.rects.size * 4)
    val indices = ArrayList<Int>(scene.rects.size * 6)
    scene.rects.forEach { rect ->
        val base = vertices.size
        val world = rectCornersWorld(rect)
        val local = rectCornersLocal(rect)
        val colors = vertexColors(rect, world, local)
        for (i in 0 until 4) {
            vertices += RectVertex(
                position = Vec4f(world[i].x.toFloat(), world[i].y.toFloat(), 0f, 1f),
                color = colors[i].toVec4f(),
                material = Vec4f(
                    rect.material.materialId.toFloat(),
                    rect.material.paramBase.toFloat(),
                    rect.objectId.toFloat(),
                    rect.material.flags.toFloat()
                ),
                shaderData = rect.material.shaderData
            )
        }
        indices += listOf(base + 0, base + 1, base + 2, base + 0, base + 2, base + 3)
    }
    return RectMesh(vertices, indices.toIntArray())
}

fun SceneProgram.lowerFrameToMesh(time: Double): RectMesh

fun SceneProgram.lowerFrameToMesh(time: Double): RectMesh =
    lowerSampledScene(sample(time))

fun SceneProgram.writeFrameToMesh

fun SceneProgram.writeFrameToMesh(
    time: Double,
    mesh: Mesh,
    attributes: RectMeshAttributeNames = RectMeshAttributeNames()
): RectMesh =
    lowerFrameToMesh(time).also { it.writeTo(mesh, attributes) }

fun SampledScene.writeToMesh

fun SampledScene.writeToMesh(
    mesh: Mesh,
    attributes: RectMeshAttributeNames = RectMeshAttributeNames()
): RectMesh =
    lowerSampledScene(this).also { it.writeTo(mesh, attributes) }

fun RectMesh.writeTo

body — 64 lines, from line 4538
fun RectMesh.writeTo(
    mesh: Mesh,
    attributes: RectMeshAttributeNames = RectMeshAttributeNames()
): Mesh {
    require(indices.size % 3 == 0) { "RectMesh indices must contain whole triangles; got ${indices.size} indices" }
    val primitiveSize = try {
        mesh.primitiveSize()
    } catch (t: UninitializedPropertyAccessException) {
        throw RectLangException(
            "Rect animation mesh writer needs a Mesh attached to a MeshModulePointer. " +
                "Construct it with Mesh(renderer, modulePointer), add it to a module first, or call from inside a module context."
        )
    }
    require(primitiveSize == 3) {
        "Rect animation mesh writer needs a triangle-list Mesh module; primitiveSize=$primitiveSize"
    }
    mesh.resize(vertices.size, indices.size / 3)
    writeRequiredVec4Attribute(mesh, attributes.position) { position }
    writeRequiredVec4Attribute(mesh, attributes.color) { color }
    writeRequiredVec4Attribute(mesh, attributes.material) { material }
    writeRequiredVec4Attribute(mesh, attributes.shaderData) { shaderData }
    val indexBuffer = mesh.t ?: throw RectLangException("Mesh did not allocate an index buffer")
    indexBuffer.mapAndWrite_ints {
        indices.forEach { put(it) }
    }
    return mesh
}

private inline fun RectMesh.writeRequiredVec4Attribute(
    mesh: Mesh,
    name: String,
    crossinline value: RectVertex.() -> Vec4f
) {
    try {
        mesh.input(name)
    } catch (t: Throwable) {
        throw RectLangException(
            "Rect animation mesh writer expected vec4 vertex attribute '$name'. " +
                "Use the standard shader contract or pass RectMeshAttributeNames for this shader. " +
                "Original error: ${t.message}"
        )
    }
    mesh.write(name) {
        vertices.forEach { vertex ->
            val v = vertex.value()
            put(v.x).put(v.y).put(v.z).put(v.w)
        }
    }
}

private fun vertexColors(rect: SampledRect, world: Array<Vec2d>, local: Array<Vec2d>): Array<Color4> =
    when (val paint = rect.fill) {
        is PaintSpec.Solid -> Array(4) { paint.color.alphaTimes(rect.alpha) }
        is PaintSpec.LinearGradient -> {
            val points = if (paint.space == GradientSpace.Local) local else world
            Array(4) { i -> paint.c0.mix(paint.c1, gradientT(points[i], paint.p0, paint.p1)).alphaTimes(rect.alpha) }
        }
    }

private fun gradientT(point: Vec2d, p0: Vec2d, p1: Vec2d): Double {
    val axis = p1 - p0
    val denom = axis dot axis
    return if (denom <= 1e-12) 0.0 else ((point - p0) dot axis) / denom
}

fun sampledFramePacket(program: SceneProgram, time: Double): FramePacket

fun sampledFramePacket(program: SceneProgram, time: Double): FramePacket {
    val sampled = program.sample(time)
    val contacts = detectContacts(sampled.rects)
    return FramePacket(
        time = time,
        mesh = lowerSampledScene(sampled),
        materialTable = program.materialTable,
        semantic = SemanticFrame(sampled.rects, contacts, frameMetrics(time, sampled.rects, emptyMap(), program.wall))
    )
}

fun colliderFor(rect: SampledRect): RectCollider

fun colliderFor(rect: SampledRect): RectCollider {
    val corners = rectCornersWorld(rect)
    val axisX = (corners[1] - corners[0]).normalized()
    val axisY = (corners[3] - corners[0]).normalized()
    return RectCollider(
        id = rect.id,
        corners = corners,
        center = rect.center,
        axes = arrayOf(axisX, axisY),
        halfExtents = Vec2d((corners[1] - corners[0]).length() * 0.5, (corners[3] - corners[0]).length() * 0.5)
    )
}

fun detectContacts(rects: List<SampledRect>): List<ContactPair>

fun detectContacts(rects: List<SampledRect>): List<ContactPair> {
    val colliders = rects.filter { it.alpha > 0.001 && it.size.x > 0.0 && it.size.y > 0.0 }.map(::colliderFor)
    val contacts = ArrayList<ContactPair>()
    for (i in 0 until colliders.size) {
        for (j in i + 1 until colliders.size) {
            satContact(colliders[i], colliders[j])?.let { contacts += it }
        }
    }
    return contacts
}

private fun satContact(a: RectCollider, b: RectCollider): ContactPair? {
    var minOverlap = Double.POSITIVE_INFINITY
    for (axis in a.axes + b.axes) {
        val pa = project(a.corners, axis)
        val pb = project(b.corners, axis)
        val overlap = min(pa.second, pb.second) - max(pa.first, pb.first)
        if (overlap < 0.0) return null
        minOverlap = min(minOverlap, overlap)
    }
    return ContactPair(a.id, b.id, minOverlap)
}

private fun project(points: Array<Vec2d>, axis: Vec2d): Pair<Double, Double> {
    var lo = Double.POSITIVE_INFINITY
    var hi = Double.NEGATIVE_INFINITY
    points.forEach {
        val p = it dot axis
        lo = min(lo, p)
        hi = max(hi, p)
    }
    return lo to hi
}

fun analyzeAnimation(program: SceneProgram, settings: AnalysisSettings = AnalysisSettings()): AnimationReport

body — 179 lines, from line 4661
fun analyzeAnimation(program: SceneProgram, settings: AnalysisSettings = AnalysisSettings()): AnimationReport {
    val fps = settings.fps ?: program.fps
    val dt = 1.0 / fps
    val frameCount = max(1, ceil(program.duration * fps).toInt())
    val sampled = (0..frameCount).map { index ->
        val t = min(program.duration, index * dt)
        program.sample(t)
    }

    val objectMetrics = linkedMapOf<String, MutableList<MotionMetrics>>()
    val frameMetrics = ArrayList<FrameSemanticMetrics>()
    val events = ArrayList<VisualEvent>()
    val warnings = ArrayList<AnimationWarning>()
    val previousContactPairs = mutableSetOf<Pair<String, String>>()
    val visibility = linkedMapOf<String, MutableList<Pair<Double, Boolean>>>()
    val moving = mutableMapOf<String, Boolean>()
    val previousDirection = mutableMapOf<String, Double>()
    val previousAngularSign = mutableMapOf<String, Int>()

    for (i in sampled.indices) {
        val current = sampled[i]
        val previous = sampled.getOrNull(i - 1)
        val previousById = previous?.rects?.associateBy { it.id }.orEmpty()
        val motions = linkedMapOf<String, MotionMetrics>()

        current.rects.forEach { rect ->
            val visible = isVisible(rect, program.wall) && rect.alpha > 0.001
            visibility.getOrPut(rect.id) { ArrayList() } += current.time to visible
            if (previous != null) {
                val old = previousById[rect.id]
                if (old != null) {
                    val metric = motionMetric(old, rect, previous.time, current.time)
                    objectMetrics.getOrPut(rect.id) { ArrayList() } += metric
                    motions[rect.id] = metric
                    val wasMoving = moving[rect.id] ?: false
                    if (!wasMoving && metric.speedMetersPerSecond > settings.startMovingSpeed) {
                        events += VisualEvent.StartMoving(current.time, rect.id, 0.75)
                        moving[rect.id] = true
                    } else if (wasMoving && metric.speedMetersPerSecond < settings.stopSpeed) {
                        events += VisualEvent.Stop(current.time, rect.id, 0.75)
                        moving[rect.id] = false
                    }
                    if (metric.speedMetersPerSecond > settings.startMovingSpeed) {
                        val lastDirection = previousDirection[rect.id]
                        if (lastDirection != null && angleDistance(lastDirection, metric.directionRadians) > settings.directionChangeRadians) {
                            events += VisualEvent.DirectionChange(current.time, rect.id, lastDirection, metric.directionRadians, 0.65)
                        }
                        previousDirection[rect.id] = metric.directionRadians
                    }
                    val sign = metric.angularVelocityRadiansPerSecond.signWithDeadband()
                    val oldSign = previousAngularSign[rect.id]
                    if (sign != 0 && oldSign != null && oldSign != 0 && sign != oldSign) {
                        events += VisualEvent.RotationReversal(current.time, rect.id, 0.65)
                    }
                    if (sign != 0) previousAngularSign[rect.id] = sign
                    settings.opacityThresholds.forEach { threshold ->
                        val a0 = old.alpha
                        val a1 = rect.alpha
                        if (a0 < threshold && a1 >= threshold) {
                            events += VisualEvent.OpacityCrossing(current.time, rect.id, threshold, rising = true, confidence = 0.7)
                        } else if (a0 >= threshold && a1 < threshold) {
                            events += VisualEvent.OpacityCrossing(current.time, rect.id, threshold, rising = false, confidence = 0.7)
                        }
                    }
                    val oldVisible = isVisible(old, program.wall) && old.alpha > 0.001
                    if (!oldVisible && visible) events += VisualEvent.EnterScreen(current.time, rect.id, 0.7)
                    if (oldVisible && !visible) events += VisualEvent.LeaveScreen(current.time, rect.id, 0.7)
                }
            }
        }

        val contacts = detectContacts(current.rects)
        val contactPairs = contacts.map { orderedPair(it.a, it.b) }.toSet()
        (contactPairs - previousContactPairs).forEach { events += VisualEvent.TouchStart(current.time, it.first, it.second, 0.75) }
        (previousContactPairs - contactPairs).forEach { events += VisualEvent.TouchEnd(current.time, it.first, it.second, 0.75) }
        previousContactPairs.clear()
        previousContactPairs += contactPairs

        frameMetrics += frameMetrics(current.time, current.rects, motions, program.wall)
        if (contacts.size >= 4) warnings += AnimationWarning.ExcessiveOverlap(current.time, contacts.flatMap { listOf(it.a, it.b) }.distinct())
    }

    frameMetrics.filter { it.averageSpeedMetersPerSecond > settings.tooMuchChangeSpeed }.forEach {
        warnings += AnimationWarning.TooMuchChange(it.time, it.averageSpeedMetersPerSecond)
    }

    visibility.forEach { (id, values) ->
        if (values.none { it.second }) warnings += AnimationWarning.ObjectNeverVisible(id)
    }
    blankIntervals(frameMetrics, settings.blankAreaThreshold).forEach { warnings += AnimationWarning.MostlyBlank(it.first, it.second) }
    val renderW = settings.renderWidth
    val renderH = settings.renderHeight
    if (renderW != null && renderH != null && program.fit != FitPolicy.Stretch) {
        val wallAspect = program.wall.width / program.wall.height
        val renderAspect = renderW / renderH
        if (abs(wallAspect - renderAspect) / wallAspect > 0.01) warnings += AnimationWarning.AspectMismatch(wallAspect, renderAspect)
    }

    return AnimationReport(
        duration = program.duration,
        frameMetrics = frameMetrics,
        objectMetrics = objectMetrics.mapValues { it.value.toList() },
        events = events.sortedBy { it.time },
        warnings = warnings
    )
}

private fun motionMetric(a: SampledRect, b: SampledRect, t0: Double, t1: Double): MotionMetrics {
    val dt = (t1 - t0).takeIf { it > 1e-12 } ?: 1.0
    val dx = b.center.x - a.center.x
    val dy = b.center.y - a.center.y
    val speed = sqrt(dx * dx + dy * dy) / dt
    val dRot = unwrapAngle(b.rotation - a.rotation)
    return MotionMetrics(
        objectId = b.id,
        time0 = t0,
        time1 = t1,
        dxMeters = dx,
        dyMeters = dy,
        speedMetersPerSecond = speed,
        directionRadians = atan2(dy, dx),
        dRotationRadians = dRot,
        angularVelocityRadiansPerSecond = dRot / dt,
        visibleAreaSquareMeters = abs(b.size.x * b.scale.x * b.size.y * b.scale.y),
        opacityChange = b.alpha - a.alpha
    )
}

private fun frameMetrics(time: Double, rects: List<SampledRect>, motion: Map<String, MotionMetrics>, wall: WallSpec): FrameSemanticMetrics {
    var count = 0
    var area = 0.0
    var weightedArea = 0.0
    var weightedVx = 0.0
    var weightedVy = 0.0
    var weightedSpeed = 0.0
    var weightedAngular = 0.0
    rects.forEach { rect ->
        val a = abs(rect.size.x * rect.scale.x * rect.size.y * rect.scale.y) * onWallFraction(rect, wall)
        if (rect.alpha > 0.001 && a > 1e-12) count++
        area += a
        val weight = a * rect.alpha.coerceIn(0.0, 1.0)
        weightedArea += weight
        val m = motion[rect.id]
        if (m != null) {
            val dt = (m.time1 - m.time0).takeIf { it > 1e-12 } ?: 1.0
            weightedVx += weight * m.dxMeters / dt
            weightedVy += weight * m.dyMeters / dt
            weightedSpeed += weight * m.speedMetersPerSecond
            weightedAngular += weight * abs(m.angularVelocityRadiansPerSecond)
        }
    }
    val denom = weightedArea.takeIf { it > 1e-12 } ?: 1.0
    return FrameSemanticMetrics(
        time = time,
        visibleObjectCount = count,
        totalVisibleAreaSquareMeters = area,
        averageVelocity = Vec2d(weightedVx / denom, weightedVy / denom),
        averageSpeedMetersPerSecond = weightedSpeed / denom,
        averageAngularVelocityRadiansPerSecond = weightedAngular / denom,
        totalOpacityWeightedArea = weightedArea
    )
}

private fun blankIntervals(metrics: List<FrameSemanticMetrics>, threshold: Double): List<Pair<Double, Double>> {
    val intervals = ArrayList<Pair<Double, Double>>()
    var start: Double? = null
    metrics.forEach {
        val blank = it.totalOpacityWeightedArea <= threshold
        if (blank && start == null) start = it.time
        val currentStart = start
        if (!blank && currentStart != null) {
            if (it.time - currentStart > 0.25) intervals += currentStart to it.time
            start = null
        }
    }
    val finalStart = start
    if (finalStart != null && metrics.isNotEmpty()) intervals += finalStart to metrics.last().time
    return intervals
}

The share of rect's bounding box that lies inside wall: 1 for a rectangle wholly on it, 0 for one wholly off.

private fun onWallFraction(rect: SampledRect, wall: WallSpec): Double {
    val corners = rectCornersWorld(rect)
    val minX = corners.minOf { it.x }; val maxX = corners.maxOf { it.x }
    val minY = corners.minOf { it.y }; val maxY = corners.maxOf { it.y }
    val (wx0, wy0, wx1, wy1) = if (wall.origin == Origin.TopLeft) listOf(0.0, 0.0, wall.width, wall.height)
        else listOf(-wall.width * 0.5, -wall.height * 0.5, wall.width * 0.5, wall.height * 0.5)
    val box = (maxX - minX) * (maxY - minY)
    if (box <= 1e-18) return if (minX >= wx0 && maxX <= wx1 && minY >= wy0 && maxY <= wy1) 1.0 else 0.0
    val overlap = max(0.0, min(maxX, wx1) - max(minX, wx0)) * max(0.0, min(maxY, wy1) - max(minY, wy0))
    return (overlap / box).coerceIn(0.0, 1.0)
}

private fun isVisible(rect: SampledRect, wall: WallSpec): Boolean {
    val corners = rectCornersWorld(rect)
    val minX = corners.minOf { it.x }
    val maxX = corners.maxOf { it.x }
    val minY = corners.minOf { it.y }
    val maxY = corners.maxOf { it.y }
    val halfW = wall.width * 0.5
    val halfH = wall.height * 0.5
    return maxX >= -halfW && minX <= halfW && maxY >= -halfH && minY <= halfH
}

class StackSLRectMaterialCompiler

class StackSLRectMaterialCompiler(
    private val convention: StackSLRectMaterialConvention = StackSLRectMaterialConvention()
) {

fun compile(module: ShaderModuleSpec): StackSLRectMaterialCompileResult

    fun compile(module: ShaderModuleSpec): StackSLRectMaterialCompileResult {
        val result = StackSLCompiler.compile(
            module.source,
            StackSLCompileOptions(
                prefix = module.prefix,
                includeHeader = false,
                declareSamplers = true,
                externalizeConstants = true
            )
        )
        val wrapper = if (result.ok) convention.wrap(module, result) else null
        return StackSLRectMaterialCompileResult(module, result, wrapper)
    }
}

data class StackSLRectMaterialConvention

data class StackSLRectMaterialConvention(
    val inputExpression: String = "vec4(r.uv, r.material.z, r.time)",
    val outputExpression: String = "vec4(stackColor.rgb * r.color.rgb, stackColor.a * r.color.a)"
) {

fun wrap(module: ShaderModuleSpec, result: StackSLCompileResult): String

    fun wrap(module: ShaderModuleSpec, result: StackSLCompileResult): String {
        val functionName = "material_${sanitizeGlslIdentifier(module.name)}"
        return buildString {
            appendLine(result.glsl)
            appendLine("vec4 $functionName(RectFrag r) {")
            appendLine("    vec4 stackInput = $inputExpression;")
            appendLine("    vec4 stackColor = ${module.prefix}(stackInput);")
            appendLine("    return $outputExpression;")
            appendLine("}")
        }
    }
}

data class StackSLRectMaterialCompileResult

data class StackSLRectMaterialCompileResult(
    val module: ShaderModuleSpec,
    val stackSL: StackSLCompileResult,
    val rectFragGlsl: String?
) {

val ok: Boolean get() = stackSL.diagnostics.none { it.severity == StackSLSeverity.ERROR }

fun compileStackSLMaterials

fun compileStackSLMaterials(
    program: SceneProgram,
    compiler: StackSLRectMaterialCompiler = StackSLRectMaterialCompiler()
): List<StackSLRectMaterialCompileResult> =
    program.materialTable.modules.map { compiler.compile(it) }

fun rectMaterialShaderCode

fun rectMaterialShaderCode(
    program: SceneProgram,
    compiler: StackSLRectMaterialCompiler = StackSLRectMaterialCompiler()
): RectMaterialShaderCode {
    val compiled = compileStackSLMaterials(program, compiler)
    val insertion = rectMaterialShaderInsertion(compiled)
    return RectMaterialShaderCode(
        insertion = insertion,
        exampleFragmentShader = rectAnimationFragmentShaderWrapper(insertion),
        compiledMaterials = compiled
    )
}

const val RECT_ANIMATION_VERTEX_SHADER: String = """

#version 450

layout(push_constant) uniform Push {
    mat4 worldToClip;
    float time;
    float wallWidth;
    float wallHeight;
} push;

layout(location = 0) in vec4 a_position;
layout(location = 1) in vec4 a_color;
layout(location = 2) in vec4 a_material;
layout(location = 3) in vec4 a_shader;

layout(location = 0) out vec2 v_uv;
layout(location = 1) out vec2 v_world;
layout(location = 2) out vec4 v_color;
layout(location = 3) out vec4 v_material;
layout(location = 4) out vec4 v_shader;

vec2 cornerUv(uint corner) {
    if (corner == 0u) return vec2(0.0, 0.0);
    if (corner == 1u) return vec2(1.0, 0.0);
    if (corner == 2u) return vec2(1.0, 1.0);
    return vec2(0.0, 1.0);
}

void main() {
    uint corner = uint(gl_VertexIndex) & 3u;
    v_uv = cornerUv(corner);
    v_world = a_position.xy;
    v_color = a_color;
    v_material = a_material;
    v_shader = a_shader;
    gl_Position = push.worldToClip * a_position;
}
"""

const val RECT_MATERIAL_SHADER_INSERTION_PREAMBLE: String = """

struct RectFrag {
    vec2 uv;
    vec2 world;
    vec4 color;
    vec4 material;
    vec4 shaderData;
    float time;
};

vec4 shadePlain(RectFrag r) {
    return r.color;
}

vec4 shadeSoftEdge(RectFrag r) {
    float edge = min(min(r.uv.x, 1.0 - r.uv.x), min(r.uv.y, 1.0 - r.uv.y));
    float softness = max(r.shaderData.x, 0.0001);
    float a = smoothstep(0.0, softness, edge);
    return vec4(r.color.rgb, r.color.a * a);
}

vec4 shadeDiagnostic(RectFrag r) {
    return vec4(r.uv, fract(r.material.z * 0.137), max(r.color.a, 0.35));
}
"""

const val RECT_ANIMATION_FRAGMENT_WRAPPER_PREAMBLE: String = """

#version 450

layout(location = 0) in vec2 v_uv;
layout(location = 1) in vec2 v_world;
layout(location = 2) in vec4 v_color;
layout(location = 3) in vec4 v_material;
layout(location = 4) in vec4 v_shader;

layout(location = 0) out vec4 outColor;

layout(push_constant) uniform Push {
    mat4 worldToClip;
    float time;
    float wallWidth;
    float wallHeight;
} push;
"""

fun rectMaterialShaderInsertion(compiledModules: List<StackSLRectMaterialCompileResult> = emptyList()): String

fun rectMaterialShaderInsertion(compiledModules: List<StackSLRectMaterialCompileResult> = emptyList()): String {
    val generated = compiledModules.mapNotNull { it.rectFragGlsl }
    return buildString {
        appendLine(RECT_MATERIAL_SHADER_INSERTION_PREAMBLE.trimIndent())
        generated.forEach {
            appendLine()
            appendLine(it)
        }
        appendLine()
        appendLine("vec4 dispatchMaterial(RectFrag r) {")
        appendLine("    int id = int(r.material.x + 0.5);")
        appendLine("    if (id == 0) return shadePlain(r);")
        appendLine("    if (id == 1) return shadeSoftEdge(r);")
        appendLine("    if (id == 2) return shadeDiagnostic(r);")
        compiledModules.filter { it.ok }.forEach {
            appendLine("    if (id == ${it.module.materialId}) return material_${sanitizeGlslIdentifier(it.module.name)}(r);")
        }
        appendLine("    return vec4(1.0, 0.0, 1.0, 1.0);")
        appendLine("}")
    }
}

fun rectAnimationFragmentShaderWrapper(materialInsertion: String): String

fun rectAnimationFragmentShaderWrapper(materialInsertion: String): String =
    buildString {
        appendLine(RECT_ANIMATION_FRAGMENT_WRAPPER_PREAMBLE.trimIndent())
        appendLine()
        appendLine(materialInsertion.trimIndent())
        appendLine()
        appendLine("void main() {")
        appendLine("    RectFrag r;")
        appendLine("    r.uv = v_uv;")
        appendLine("    r.world = v_world;")
        appendLine("    r.color = v_color;")
        appendLine("    r.material = v_material;")
        appendLine("    r.shaderData = v_shader;")
        appendLine("    r.time = push.time;")
        appendLine("    outColor = dispatchMaterial(r);")
        appendLine("}")
    }

fun rectAnimationFragmentShader(compiledModules: List<StackSLRectMaterialCompileResult> = emptyList()): String

fun rectAnimationFragmentShader(compiledModules: List<StackSLRectMaterialCompileResult> = emptyList()): String =
    rectAnimationFragmentShaderWrapper(rectMaterialShaderInsertion(compiledModules))

data class RectSLRenderSettings

data class RectSLRenderSettings(
    var pixelsPerMeter: Double,
    var fps: Double,
    var outputDirectory: Path,
    var renderer: Renderer,
    var renderTarget: RenderTarget = renderer.renderList,
    var name: String = "rectsl-${UUID.randomUUID().toString().take(8)}",
    var jpegQuality: Int = 92,
    var includeFinalFrame: Boolean = false,
    var frameName: (Int, Double) -> String = { index, _ -> "frame_%05d.jpg".format(index) },
    var timeoutMsPerFrame: Long = 15_000L,
    var waitPollMs: Long = 8L,
    var driveRendererWhileWaiting: Boolean = false
) {

fun validate()

    fun validate() {
        require(pixelsPerMeter > 0.0) { "pixelsPerMeter must be positive" }
        require(fps > 0.0) { "fps must be positive" }
        require(jpegQuality in 1..100) { "jpegQuality must be in 1..100" }
        require(timeoutMsPerFrame > 0L) { "timeoutMsPerFrame must be positive" }
        require(waitPollMs >= 0L) { "waitPollMs cannot be negative" }
    }
}

data class RectSLRenderResult

data class RectSLRenderResult(
    val program: SceneProgram,
    val width: Int,
    val height: Int,
    val fps: Double,
    val frameTimes: List<Double>,
    val files: List<Path>
) {

val frameCount: Int get() = files.size

fun interface RectSLRenderProgress

fun frameSaved(frameIndex: Int, time: Double, file: Path)

object RectSLRenderRunner

suspend fun renderJpgSequence

    suspend fun renderJpgSequence(
        source: String,
        settings: RectSLRenderSettings,
        progress: RectSLRenderProgress? = null
    ): RectSLRenderResult {
        val compile = RectAnimationOracle.compile(source)
        if (!compile.ok || compile.programOrNull == null) {
            throw RectLangException("RectSL compile failed:\n${compile.diagnostics.joinToString("\n")}")
        }
        return renderJpgSequence(compile.program, settings, progress)
    }

suspend fun renderJpgSequence

body — 89 lines, from line 5105
    suspend fun renderJpgSequence(
        program: SceneProgram,
        settings: RectSLRenderSettings,
        progress: RectSLRenderProgress? = null
    ): RectSLRenderResult {
        settings.validate()
        val shaderCode = rectMaterialShaderCode(program)
        if (!shaderCode.ok) {
            val diagnostics = shaderCode.compiledMaterials
                .flatMap { it.stackSL.diagnostics }
                .joinToString("\n") { "${it.severity}: ${it.message}" }
            throw RectLangException("RectSL material compile failed:\n$diagnostics")
        }

        Files.createDirectories(settings.outputDirectory)
        val width = max(1, round(program.wall.width * settings.pixelsPerMeter).toInt())
        val height = max(1, round(program.wall.height * settings.pixelsPerMeter).toInt())
        val frameTimes = rectSLFrameTimes(program.duration, settings.fps, settings.includeFinalFrame)
        val renderer = settings.renderer
        val targetName = "${settings.name}-rtt"
        val moduleName = "${settings.name}-module"
        val meshName = "${settings.name}-mesh"

        var currentTime = 0.0
        val worldToClip = program.wall.worldToClip()
        val rtt = RenderToTexture(renderer) {
            this.width = width
            this.height = height
            this.format = VK_FORMAT_B8G8R8A8_SRGB
            this.useDepth = false
            this.clearColor = afield.math.linalg.vec(0f, 0f, 0f, 1f)
            this.colorTransferSrc = true
        }
        val module = Module(parameters(renderer) {
            name = moduleName
            vertex = RECT_ANIMATION_VERTEX_SHADER
            fragment = shaderCode.exampleFragmentShader
            blend = true
            depthTest = false
            primitiveType = triangles
            +rtt
        }).pointer()
        val mesh = Mesh(renderer, module)
        mesh.push["worldToClip"] = worldToClip
        mesh.push.bind("time") { currentTime.toFloat() }
        mesh.push["wallWidth"] = program.wall.width.toFloat()
        mesh.push["wallHeight"] = program.wall.height.toFloat()
        mesh.update["rectsl-frame"] = sequence {
            while (true) {
                program.writeFrameToMesh(currentTime, mesh)
                yield(Unit)
            }
        }
        module.current.meshes[meshName] = mesh
        rtt += moduleName to module
        val previousPreTarget = settings.renderTarget.preTargets.put(targetName, rtt)

        val files = ArrayList<Path>(frameTimes.size)
        try {
            frameTimes.forEachIndexed { index, time ->
                currentTime = time
                val file = settings.outputDirectory.resolve(settings.frameName(index, time))
                Files.createDirectories(file.parent ?: settings.outputDirectory)
                Files.deleteIfExists(file)
                rtt.saveJpgAsync(file.toString(), quality = settings.jpegQuality)
                    .awaitRectSLSave(renderer, settings)
                files.add(file)
                progress?.frameSaved(index, time, file)
            }
        } finally {
            if (previousPreTarget == null) settings.renderTarget.preTargets.remove(targetName)
            else settings.renderTarget.preTargets[targetName] = previousPreTarget
            rtt.modules.remove(moduleName)
            module.current.meshes.remove(meshName)
            mesh.cleanupRectSLBuffers()
            module.current.cleanup()
            rtt.cleanupRectSLResources()
        }

        return RectSLRenderResult(
            program = program,
            width = width,
            height = height,
            fps = settings.fps,
            frameTimes = frameTimes,
            files = files
        )
    }
}

suspend fun renderRectSLJpgSequence

suspend fun renderRectSLJpgSequence(
    source: String,
    outputDirectory: Path,
    pixelsPerMeter: Double,
    fps: Double,
    renderer: Renderer = fromContext(),
    configure: RectSLRenderSettings.() -> Unit = {}
): RectSLRenderResult {
    val settings = RectSLRenderSettings(
        pixelsPerMeter = pixelsPerMeter,
        fps = fps,
        outputDirectory = outputDirectory,
        renderer = renderer
    ).apply(configure)
    return RectSLRenderRunner.renderJpgSequence(source, settings)
}

suspend fun renderRectSLJpgSequence

suspend fun renderRectSLJpgSequence(
    source: String,
    outputDirectory: Path,
    renderer: Renderer = fromContext(),
    configure: RectSLRenderSettings.() -> Unit
): RectSLRenderResult {
    val settings = RectSLRenderSettings(
        pixelsPerMeter = 100.0,
        fps = 60.0,
        outputDirectory = outputDirectory,
        renderer = renderer
    ).apply(configure)
    return RectSLRenderRunner.renderJpgSequence(source, settings)
}

suspend fun renderRectSLJpgSequence

suspend fun renderRectSLJpgSequence(
    source: String,
    outputDirectory: String,
    pixelsPerMeter: Double,
    fps: Double,
    renderer: Renderer = fromContext(),
    configure: RectSLRenderSettings.() -> Unit = {}
): RectSLRenderResult =
    renderRectSLJpgSequence(source, Path.of(outputDirectory), pixelsPerMeter, fps, renderer, configure)

suspend fun renderRectSLJpgSequence

suspend fun renderRectSLJpgSequence(
    source: String,
    outputDirectory: String,
    renderer: Renderer = fromContext(),
    configure: RectSLRenderSettings.() -> Unit
): RectSLRenderResult =
    renderRectSLJpgSequence(source, Path.of(outputDirectory), renderer, configure)

fun renderRectSLJpgSequenceBlocking

fun renderRectSLJpgSequenceBlocking(
    source: String,
    outputDirectory: Path,
    pixelsPerMeter: Double,
    fps: Double,
    renderer: Renderer = fromContext(),
    configure: RectSLRenderSettings.() -> Unit = {}
): RectSLRenderResult =
    runBlocking {
        renderRectSLJpgSequence(source, outputDirectory, pixelsPerMeter, fps, renderer, configure)
    }

fun renderRectSLJpgSequenceBlocking

fun renderRectSLJpgSequenceBlocking(
    source: String,
    outputDirectory: Path,
    renderer: Renderer = fromContext(),
    configure: RectSLRenderSettings.() -> Unit
): RectSLRenderResult =
    runBlocking {
        renderRectSLJpgSequence(source, outputDirectory, renderer, configure)
    }

private fun rectSLFrameTimes(duration: Double, fps: Double, includeFinalFrame: Boolean): List<Double> {
    val baseCount = max(1, ceil(duration * fps).toInt())
    val frameCount = if (includeFinalFrame && duration > 0.0) baseCount + 1 else baseCount
    return List(frameCount) { index ->
        if (includeFinalFrame && frameCount > 1) {
            duration * index.toDouble() / (frameCount - 1).toDouble()
        } else {
            min(duration, index.toDouble() / fps)
        }
    }
}

fun WallSpec.worldToClip(): Matrix4f

Orthographic world-to-clip projection respecting this wall's dimensions and origin.

body — 99 lines, from line 5280
fun WallSpec.worldToClip(): Matrix4f =
    when (origin) {
        Origin.Center -> Matrix4f().ortho(
            (-width * 0.5).toFloat(),
            (width * 0.5).toFloat(),
            (-height * 0.5).toFloat(),
            (height * 0.5).toFloat(),
            -1f,
            1f
        )
        Origin.TopLeft -> Matrix4f().ortho(
            0f,
            width.toFloat(),
            height.toFloat(),
            0f,
            -1f,
            1f
        )
    }

private suspend fun CompletableFuture<String>.awaitRectSLSave(
    renderer: Renderer,
    settings: RectSLRenderSettings
): String {
    val deadline = System.nanoTime() + settings.timeoutMsPerFrame * 1_000_000L
    while (!isDone) {
        if (settings.driveRendererWhileWaiting) {
            renderer.render()
        }
        if (System.nanoTime() > deadline) {
            cancel(true)
            throw RectLangException("Timed out waiting for RectSL JPEG save after ${settings.timeoutMsPerFrame}ms")
        }
        if (settings.waitPollMs > 0L) delay(settings.waitPollMs)
        else kotlinx.coroutines.yield()
    }
    return withContext(Dispatchers.IO) {
        get(1L, TimeUnit.MILLISECONDS)
    }
}

private fun Mesh.cleanupRectSLBuffers() {
    (v.values + vn.values).toSet().forEach { it.cleanup() }
    t?.cleanup()
    v.clear()
    vn.clear()
    t = null
}

private fun RenderToTexture.cleanupRectSLResources() {
    defaultSampler?.cleanup()
    imageViewArray.map { it.imageView }.distinctBy { it.vkImageView }.forEach { it.cleanup() }
    colorImageRefs.filterNotNull().distinctBy { it.vkImage }.forEach { it.cleanup() }
    listOfNotNull(depthSampledImageView, depthImageView).distinctBy { it.vkImageView }.forEach { it.cleanup() }
    depthImage?.cleanup()
    commandBuffers.forEach { it.cleanup() }
    fences.forEach { it.cleanup() }
}

private fun rotate(p: Vec2d, angle: Double): Vec2d {
    val c = cos(angle)
    val s = sin(angle)
    return Vec2d(c * p.x - s * p.y, s * p.x + c * p.y)
}

private fun positiveModulo(x: Double, period: Double): Double {
    if (period <= 1e-12) return x
    val p = x % period
    return if (p < 0.0) p + period else p
}

private fun unwrapAngle(angle: Double): Double {
    var a = angle
    while (a > PI) a -= PI * 2.0
    while (a < -PI) a += PI * 2.0
    return a
}

private fun angleDistance(a: Double, b: Double): Double = abs(unwrapAngle(b - a))

private fun Double.signWithDeadband(deadband: Double = 1e-5): Int =
    when {
        this > deadband -> 1
        this < -deadband -> -1
        else -> 0
    }

private fun orderedPair(a: String, b: String): Pair<String, String> = if (a <= b) a to b else b to a

private fun sanitizeGlslIdentifier(name: String): String =
    buildString {
        name.forEachIndexed { index, c ->
            val ok = c == '_' || c.isLetterOrDigit()
            val next = if (ok) c else '_'
            if (index == 0 && next.isDigit()) append('_')
            append(next)
        }
        if (isEmpty()) append("material")
    }

object RectSLSmokeTest

val minimalAcceptanceSource: String

    val minimalAcceptanceSource: String = """
        ->wall 30 4 meters
        ->duration 10
        ->fps 60

        "r" rect
          -5 0 ->xy
          1 ->square
          0xff3300 solid ->fill
          '( 0=-5  5=0  10=5 ) ->x
        ;

        "r" ->select clone  "r2" ->rename
          ->mirror-x
          0x3333ff solid ->fill
        ;
    """.trimIndent()

fun main(args: Array<String>)

    @JvmStatic
    fun main(args: Array<String>) {
        val result = RectAnimationOracle.compile(minimalAcceptanceSource)
        check(result.ok && result.programOrNull != null) { result.diagnostics.joinToString("\n") }

        val program = result.program
        val rects = program.nodes.filterIsInstance<RectNode>()
        check(rects.map { it.id }.toSet() == setOf("r", "r2")) { rects.map { it.id } }

        val sampled = program.sample(5.0).rects.associateBy { it.id }
        check(sampled.keys == setOf("r", "r2")) { sampled.keys }
        check(abs(sampled.getValue("r").center.x) < 1e-6)
        check(abs(sampled.getValue("r2").center.x) < 1e-6)

        val mesh = lowerSampledScene(program.sample(5.0))
        check(mesh.vertices.size == 8) { "Expected 8 vertices, got ${mesh.vertices.size}" }
        check(mesh.indices.size == 12) { "Expected 12 indices, got ${mesh.indices.size}" }
        check(mesh.vertices.all { it.position.z == 0f }) { "RectSL mesh positions should stay in clip-space z" }
        check(mesh.vertices.map { it.material.z }.toSet() == setOf(0f, 1f)) { "Expected object ids in material.z" }
        println("rectsl smoke passed")
    }
}