Skip to content

Visualization

Turning a value into something you can look at, without leaving the box.

62 source file(s), 1728 public symbol(s), 1235 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.

keyframeBrowser.kt

Reading a collection of saved keys the way KeyframeScope reads one: a glob of key files in, a slot-major summary out.

Why slot-major

A directory of keys is a sparse three-axis cube — file × version × slot — and only one of those axes tells you anything you did not already know. You know your file names. What you cannot see from a directory listing is which slots you actually kept pushing on, which have been frozen since the first save, and which are missing from half the files because the scope grew a target after they were written. So this pivots to slots, and every summary here is computed across files rather than within one.

What load reads

load decodes each matched file straight to KeyDTO rather than going through KeyframeScope.loadKey, which fills KeyframeScope.missingKey defaults for names a file lacks — exactly right when you are about to apply a key and exactly wrong when you are trying to see which names a file lacks. The browser wants the file as written.

Version history is read lazily. load calls KeyframeScope.keyVersions for each file, which lists a directory and parses one manifest, but it does not open the numbered snapshots. Those are only read when slotHistory is asked for one slot's trajectory — a hundred saves of ten files is a thousand files, and no one wants that on open.

Failure is per-file

A collection with one unparseable file still browses. load records the failure in KeyframeCollection.problems and carries on, so one bad file does not make the other nine invisible.

src/jvmMain/kotlin/afield/animation/keyframes/keyframeBrowser.kt — read the source

Top-level functions

Declaration Summary
fun keyFilesMatching(root: Path, pattern: String = everyKeyGlob): List<Path> Every regular file under root whose path relative to root matches the glob pattern, sorted by path, excluding anything inside a .versions directory.
fun friendlyTime(timestamp: String, now: Instant = Instant.now(), zone: ZoneId = ZoneId.systemDefault()): String A save's timestamp as a person reads it: relative while that is the useful thing to know, and an ordinary date once it is not.

Top-level values

Declaration Summary
const val everyKeyGlob: String The glob that means "every key in this directory and below", and the default wherever a key collection is opened.

SkipKind

enum class SkipKind

Why a file under the collection root is not a row in the browser.

Constants: NotAKey, Damaged, Hidden.

SkippedFile

data class SkippedFile(val relative: String, val kind: SkipKind, val detail: String)

One file under the root that is not a browser row, and the one line explaining that.

Declaration Summary
val relative: String Path relative to the collection root, /-separated.
val kind: SkipKind
val detail: String One human-readable line. Never a stack trace.

KeyframeFile

data class KeyframeFile(val id: Int, val path: Path, val relative: String, val schemaVersion: Int, val values: Map<String, KValue>, val versions: List<KeyVersionInfo>)

One key file as the browser sees it: what is on disk, plus how many times it has been saved.

Declaration Summary
val id: Int Stable index within the collection, used as the wire identity for selection and diffing.
val path: Path
val relative: String path relative to the collection root, with / separators — what the UI shows.
val schemaVersion: Int KeyDTO.schemaVersion as written. Zero both when it was written as zero and when absent.
val values: Map<String, KValue> The file's entries exactly as written — no KeyframeScope.missingKey defaults applied.
val versions: List<KeyVersionInfo> The save history from <file>.versions, oldest first; empty when nothing has been saved.
val latest: KeyVersionInfo? The most recent history row, which carries the note and timestamp the file name cannot.

KeyframeCollection

class KeyframeCollection(val root: Path, val pattern: String, val files: List<KeyframeFile>, val problems: List<String>, val scopeSlots: Set<String> = emptySet(), val scopeAttached: Boolean = false, val skipped: List<SkippedFile> = emptyList(), val skipSurveyTruncated: Boolean = false)

A loaded collection of key files and the slot pivot over them.

Declaration Summary
val root: Path
val pattern: String
val files: List<KeyframeFile>
val problems: List<String> Per-file failures, one human-readable line each. A collection with problems still browses.
val scopeSlots: Set<String> Slot names the live scope watches, from scope.capture().values.keys. Used to mark a slot the scope has but no file does — the "you have never saved this" row, which is the one thing a collection cannot tell you about itself.
val scopeAttached: Boolean Whether a live scope was passed to load at all, which is not the same question as whether scopeSlots is empty. The orphan marker below needs both answers.
val skipped: List<SkippedFile> Every file under root that is not a row here, and why — the browser's account of what it did not show you. See SkipKind for the three reasons and skipSummary for the one line.
val skipSurveyTruncated: Boolean True when the hidden-key survey stopped at HIDDEN_SCAN_BUDGET rather than finishing. The counts below are then floors, not totals, and skipSummary says so.
val hiddenCount: Int Readable keys that the pattern excluded: keys on disk the glob did not reach.
val ignoredCount: Int Files that were never keys. A notice, not an alarm.
fun skipSummary(): String? One line for what the browser did not show, or null when it showed everything.
val slotNames: List<String> Every slot name in any file, plus any scopeSlots no file has, sorted.
val versionCount: Int
val schemaVersions: List<Int> The distinct KeyDTO.schemaVersions present, ascending — more than one means schema drift.
val latest: KeyVersionInfo? The newest history row across every file, by timestamp.
fun file(id: Int): KeyframeFile?
fun slots(): List<SlotSummary> The pivot: one SlotSummary per name in slotNames, in the same order.
fun slot(name: String): SlotSummary The summary for one slot — what value it takes in each file, and what that set looks like.
val orphanedSlots: List<String> Slot names that some key on disk holds and the live scope does not watch — the saves that now apply to nowhere.
fun slotHistory(fileId: Int, slot: String): List<Pair<KeyVersionInfo, KValue?>> One slot's trajectory through a single file's save history, oldest first — the view that answers "what did I actually keep pushing on".

SlotBand

data class SlotBand(val label: String, val lo: Double, val hi: Double, val marks: List<Double>)

A low–high band with the individual values marked inside it, as a range strip is drawn.

Declaration Summary
val label: String
val lo: Double
val hi: Double
val marks: List<Double> Each value's position in 0..1 across lo..hi. A degenerate band puts every mark at 0.5.

SlotVariant

data class SlotVariant(val text: String, val preview: String, val count: Int, val fileIds: List<Int>)

One distinct value of a slot that varies discretely, and which files hold it.

Declaration Summary
val text: String Full text — source for a code slot, the string itself for a string slot.
val preview: String First line, clipped, for the collapsed row.
val count: Int
val fileIds: List<Int>

SlotDisplay

sealed interface SlotDisplay

How a slot's spread should be drawn. The split is not cosmetic: it follows KeyframeScope.interpolate, which blends numbers and vectors and steps strings, StackSL source and source blocks. A slot that steps has no range, only a set of variants, and drawing a bar for one would claim a continuum that does not exist.

Declaration Summary
data class Range(val band: SlotBand, val fileIds: List<Int>) : SlotDisplay A single numeric band.
data class Components(val bands: List<SlotBand>, val fileIds: List<Int>) : SlotDisplay One band per component — x/y/z, or the four of a quaternion.
data class Vec4Plot(val xy: List<List<Double>>, val zw: List<List<Double>>, val colors: List<String>? = null, val length: SlotBand, val fileIds: List<Int>, val bands: List<SlotBand>) : SlotDisplay A vec4, as two 2D plots and a magnitude.
data class Poses(val points: List<List<Double>>, val fileIds: List<Int>, val arcDegrees: Double, val bands: List<SlotBand>) : SlotDisplay A rigid transform or camera pose: the translations as a 2D scatter (x across, z up — the plan view a camera move is usually read in), the orientation spread as an angle.
data class Variants(val variants: List<SlotVariant>) : SlotDisplay A slot that steps rather than blends.
data class Note(val text: String, val severity: String = "info") : SlotDisplay Nothing to draw: the slot is absent, or holds types that cannot be summarised together.

SlotSummary

data class SlotSummary(val name: String, val kind: String, val typeLabel: String, val presentIn: Int, val total: Int, val inScope: Boolean, val orphaned: Boolean, val summaryText: String, val display: SlotDisplay)

The pivot's output for one slot name.

Declaration Summary
val name: String
val kind: String The KValue serial name shared by every present value, "mixed", or "absent".
val typeLabel: String Human label for the type column — float, vec4, pose, stacksl, mixed (num, string).
val presentIn: Int
val total: Int
val inScope: Boolean True when a live scope watches this name.
val orphaned: Boolean True when a key on disk holds this slot and the attached scope does not watch it — a saved value that Apply will silently drop.
val summaryText: String The right-hand column: 2.0 – 11.4, 7 swatches, 3 variants.
val display: SlotDisplay

LorisVisualizer.kt

Draws a LorisModel's partial trajectories as an interactive HTML panel next to its waveform.

visualizationHtml plots each partial's breakpoints at their real times on a cents, ERB or Hz axis (LorisFrequencyAxis), shares the time window, cursor and click-to-seek with the waveform view, and caps how many partials and points are drawn (LorisViewOptions). The caps affect drawing only.

src/jvmMain/kotlin/afield/audio/analysis/loris/LorisVisualizer.kt — read the source

Used in: Loris: partial tracks and resynthesis

Top-level functions

Declaration Summary
fun LorisModel.visualizationHtml(options: LorisViewOptions = LorisViewOptions()): String Actual irregular trajectories, with shared waveform time-window, cursor and click-to-seek events.
fun LorisResult.visualizationHtml(options: LorisViewOptions = LorisViewOptions()): String Adds a real reconstruction player to the synchronized partial map. PCM data itself is unchanged.

LorisFrequencyAxis

enum class LorisFrequencyAxis

Frequency scale of the trajectory plot.

Constants: CENTS, ERB, HZ.

LorisTrackSelection

enum class LorisTrackSelection

Which partials fill the drawing budget: spread across ERB bands, or the strongest overall.

Constants: BAND_BALANCED, STRONGEST.

LorisViewOptions

data class LorisViewOptions(val width: Int = 1000, val height: Int = 380, val minimumHz: Double = 0.0, val maximumHz: Double? = null, val axis: LorisFrequencyAxis = LorisFrequencyAxis.ERB, val selection: LorisTrackSelection = LorisTrackSelection.BAND_BALANCED, val maxPartials: Int = 320, val pointsPerPartial: Int = 1500, val syncGroup: String = "", val label: String = "Loris partial trajectories")

Display limits affect drawing only, never the stored native model or resynthesis.

Declaration Summary
val width: Int
val minimumHz: Double
val axis: LorisFrequencyAxis
val selection: LorisTrackSelection
val maxPartials: Int
val syncGroup: String

interactionnets.kt

Interaction nets as a live-codeable medium: a graph whose nodes rewrite each other wherever two of them touch principal port to principal port, with a DSL for building nets, a rule language for the rewrites, a scheduler that decides which collision fires next, and a force-directed layout so you can watch it happen.

Start at net to build one, rules to say how it rewrites, Reducer to run it, and Net.toFLines or Box.showInteractionNet to look at it. Lambda is a ready-made vocabulary (APP, LAM, DUP, ERASE, ROOT, VAR, CONST) with its own rule book, if you want the classical behaviour rather than your own.

A compact, single-file interaction-net playground meant to be easy to lift into a larger codebase, but also meant to be a fertile computational substrate for live coding art.

What is in here: - a small in-memory interaction-net graph model - a Kotlin DSL for building whole nets and reusable graph fragments - two rewrite styles: 1) a declarative "automatic rewiring" DSL built around ext(side, aux) 2) an escape hatch for arbitrary custom rewrites in ordinary Kotlin - configurable active-pair scheduling - a simple force-directed layout with incremental seeding of rewrite RHS nodes at the LHS center - FLine/output rendering plus SVG export for quick inspection - a small lambda-oriented bundle including APP, LAM, DUP, ERASE, ROOT, VAR, CONST

Design intent: - Keep the core tiny and hackable. - Make the common case pleasant without removing low-level control. - Be explicit about where the model is still incomplete. - Treat the system less like a theorem prover and more like a graph chemistry / reactive medium.

The artistic framing: This file is not most interesting as a route to "real lambda calculus". It is more interesting as a local, spatial, rewiring-based substrate for live coding. In that framing:

  • principal ports are not just logical redex triggers; they are collision points, activation sites, rhythmic contact surfaces
  • auxiliary ports are not just variables; they can be treated as tension channels, color buses, growth directions, continuation hooks, memory strands, or signal paths
  • rewrite rules are not merely reductions; they are local laws of motion, branching, fusion, mutation, decay, reflection, gating, or pulse propagation
  • active-pair scheduling is not merely evaluation order; it is rhythm, phrasing, emphasis, groove, cascade control
  • layout is not merely display; it is motion memory and can become part of the meaning of the system

DUP and ERASE in this context: DUP and ERASE are useful here less as strict lambda-calculus machinery and more as artistic growth and decay primitives.

  • DUP can mean branching, echo, motif propagation, splitting a line of force into two descendants, or generating two related but diverging futures
  • ERASE can mean fade, pruning, absorption, dissipation, culling, or a sink that removes pressure from a local region

In a more expressive live system, DUP does not need to be perfectly literal duplication. It can create two descendants with slight positional offsets, inherited state, hue drift, phase drift, timing skew, energy split, or generation markers. Likewise ERASE does not need to be instantaneous deletion; it can become gradual attenuation, shrinkage, or multi-frame disappearance.

Why this is promising for live coding art: - Local causality: every visible change comes from a small neighborhood interaction. - Spatial meaning: the graph is data and choreography at once. - Compositionality: a small table of rewrite laws can yield rich large-scale behavior. - Performability: rules, scheduler, layout constants, render mapping, and node injection can all be changed while the system is running.

Activation order as an instrument: Reduction order is a first-class artistic control. A deterministic scheduler can feel mechanical; randomized scheduling can shimmer; energy-biased or cursor-biased scheduling can create bursts, avalanches, attraction, or spotlighting. Treat scheduling as part of the score.

Layout as part of computation: The incremental layout already gives new RHS nodes a positional memory by seeding them at the centroid of the consumed active pair before relaxation. That idea can be pushed further. Rewrite rules can read or influence geometry; branch direction can depend on current edge angle; replication distance can depend on energy; eligibility can depend on local spatial conditions; layout velocity can feed rendering or sound. The graph need not be computed first and visualized second.

A "wet" semantics is often better than a perfectly dry one: For artistic purposes, clean instantaneous replacement is sometimes less interesting than sticky, physicalized replacement. Newly created nodes can inherit centroid and velocity. Consumed edges can leave ghosts. Erased nodes can decay over a few frames. Duplicated nodes can repel strongly at birth and then settle. Rules can emit transient sparks or decorative residues. These choices improve visual legibility and expressiveness during rapid change.

The next high-leverage extension is per-node state: Adding a small mutable state record to each node would make the substrate much richer. Examples:

data class State(
    var energy: Double = 1.0,
    var phase: Double = 0.0,
    var hue: Double = 0.0,
    var age: Double = 0.0
)

Then rewrite rules can split energy on DUP, accumulate age, drift hue, mutate phase, and directly map these quantities to radius, stroke weight, opacity, curvature, synthesis parameters, or triggering.

Rendering and sound hooks: A rewrite can be more than a topology update. In a live system it is natural for a rewrite to also emit events such as "flash this edge", "trigger a note", "spawn a particle", "increase camera pull", or "mark this component active". That turns graph rewriting into the event engine for an audiovisual system.

Useful node archetypes for an artistic palette: - Pulse : propagates motion or activation - DUP : bifurcates structure or motive - ERASE : consumes / attenuates / prunes - Delay : holds activity for N frames or beats - Gate : conditionally passes or blocks interaction - Mirror : swaps neighborhoods / reflects flow - Fuse : merges strands - Noise : injects stochastic variation - Anchor : spatial stabilizer - Bloom : expands into decorative local structure

A note on rigor: This file adds DUP and ERASE agents in the usual explicit-resource spirit, but it is not trying to be a complete optimal reducer. There is no full fan/fan commuting story, no full lambda frontend, and no claim of theoretical completeness. That is deliberate. The value here is as a hackable medium with:

  • topology that changes
  • space that remembers
  • order that matters
  • small laws that create style

In other words: treat this as a graph chemistry for live coding.

src/jvmMain/kotlin/afield/data/graph/interactionnets.kt — read the source

Used in: Interaction Nets examples, A λ-term you can hear

Top-level functions

Declaration Summary
fun fragment(block: FragmentBuilder.() -> Unit): NetFragment Builds a reusable NetFragment: fragment { val a = node("a", APP); expose("in", principal(a)) }.
fun net(block: NetBuilder.() -> Unit): Net Builds a Net: net { val a = node(APP); val b = node(LAM); a.principal to b.principal }.
fun rules(block: RuleBook.() -> Unit): RuleBook Builds a RuleBook: rules { autoRule("beta", APP, LAM) { … } }.
fun Net.writeSvg(path: String, width: Int = 900, height: Int = 700) Writes the net as an SVG file at path — Net.toSvg straight to disk, on white.
fun Box.showInteractionNet(net: Net, channel: String = "interaction-net", summaryChannel: String? = "$channel-summary", title: String = "Interaction net", w: Double = 920.0, h: Double = 560.0, fitToSize: Boolean = true): List<FLine> Draws the net into this box's output and puts its summary card beside it — the one call a live sketch needs per frame.
fun Box.showInteractionSummary(channel: String = "interaction-net-summary", vararg cards: String) Displays several HTML cards side by side in one responsive grid in this box's output.
fun Box.showInteractionOverview(graphicLines: List<FLine>, cards: List<String>, channel: String = "interaction-net-overview", graphicTitle: String = "Comparison", graphicWidth: Double = 1040.0, graphicHeight: Double = 340.0): String Displays a row of summary cards with one shared drawing underneath, as a single HTML block, and returns that HTML.
fun interactionSummaryGrid(vararg cards: String): String Wraps HTML cards in a responsive grid — columns of at least 260px, as many as fit — and returns the markup.
fun stage(title: String, lines: List<FLine>, summary: String): InteractionStage Capture a stage now, before a later rewrite mutates the net or its drawing.
fun Box.showInteractionStages(vararg stages: InteractionStage, cards: List<String> = emptyList(), channel: String = "interaction-net-overview", graphicTitle: String = stages.joinToString(" → ") { it.title }, stageWidth: Double = 404.0, stageHeight: Double = 232.0, gap: Double = 128.0, padding: Double = 26.0): String Lay out captured stages in order, with arrows between their drawings and summaries above. Rewriting and snapshot capture remain explicit in the caller. Extra cards can describe the intervening reductions. Uses showInteractionOverview for the shared SVG/output rendering.
fun ReductionStep.summaryHtml(title: String = "Reduction step"): String An HTML card describing one reaction: the rule that fired, whether the pair was matched swapped, which node ids went and which arrived, and every event emitted.
fun main() A worked example of the whole pipeline, as a runnable main: build each Lambda demo net, relax it, reduce it and write before/after SVGs.

LayoutConfig

data class LayoutConfig(val repulsion: Double = 2500.0, val springLength: Double = 90.0, val springStrength: Double = 0.015, val damping: Double = 0.82, val timeStep: Double = 1.0, val jitter: Double = 8.0, val clampStep: Double = 20.0)

The knobs on the force-directed layout that Net.relax runs — every node pushing every other apart, every edge pulling its two ends together.

Declaration Summary
val repulsion: Double How hard every node pushes every other away. Falls off as 1/d², so it sets the empty space between unconnected nodes without much affecting connected ones.
val springLength: Double The distance an edge is happy at, in position units. The quickest single knob for the overall scale of the picture.
val springStrength: Double How strongly an edge pulls back toward springLength. Large values make the net rigid and jittery.
val damping: Double Velocity retained per step. Below 1 is what makes the simulation settle at all; toward 1 it wobbles for far longer, and at 1 it never stops.
val timeStep: Double Integration step. Scales both the velocity update and the movement, so raising it is a faster but less stable simulation.
val jitter: Double Stored but unused by the current layout step — new nodes get their initial scatter from Net.create instead.
val clampStep: Double Per-step speed limit, in position units. What stops two nearly coincident nodes from firing each other off the canvas.

NodeState

data class NodeState(var energy: Double = 1.0, var phase: Double = 0.0, var hue: Double = 0.0, var age: Double = 0.0, val scalars: MutableMap<String, Double> = linkedMapOf())

The mutable per-node payload that turns a bare interaction net into something worth watching: four named quantities the renderer already reads, plus scalars for anything else you want to carry.

Declaration Summary
var energy: Double Drives node radius when drawn, and is what the energy-biased PairSelectors rank pairs by. Every operation here floors it at 0, but nothing caps it.
var phase: Double Carried and split, but nothing in this file interprets it — yours to use, though the renderer does fall back to it for a node's port angle when the node has no connected neighbours.
var hue: Double Drives colour, as a fraction of a turn. Wrapped at render time rather than here, so it may drift far outside 0..1 and that is fine.
var age: Double Advanced only by Net.incrementAges, in whatever unit you pass it — so age runs on the clock you choose. Nudges drawn lightness.
val scalars: MutableMap<String, Double> Room for your own named quantities. Deep-copied by copyDeep and blended key-wise by blend, where a key missing from one side counts as 0.
fun copyDeep(): NodeState An independent copy, scalars map included, so later mutation of either cannot reach the other.
fun evolved(energyScale: Double = 1.0, energyBias: Double = 0.0, phaseShift: Double = 0.0, hueShift: Double = 0.0, ageDelta: Double = 0.0): NodeState A copy with the four quantities moved on — the "one node becomes one node, a little different" step for a rewrite rule to hand its replacement.
fun split(parts: Int, branch: Int, attenuation: Double = 1.0, hueSpread: Double = 0.0, phaseSpread: Double = 0.0, ageDelta: Double = 0.0): NodeState One branch's share of this state when a node divides into parts — the state side of a DUP-style rewrite.

PortKind

enum class PortKind

Which of a node's two kinds of port this is — and therefore whether an edge through it can react.

Constants: PRINCIPAL, AUXILIARY.

AgentType

data class AgentType(val name: String, val auxPorts: Int)

A kind of node: a name to match rules against, and how many auxiliary ports its instances get.

Declaration Summary
val arity: Int Total port count: the auxiliaries plus the one principal port.

Port

class Port internal constructor(val node: Node, val index: Int, val kind: PortKind)

One port on one node — the thing edges connect. Port 0 is always the principal port and 1..auxPorts the auxiliaries, which is why Node.aux takes a 0-based index and adds one for you.

Declaration Summary
val node: Node The node this port belongs to. A port never moves between nodes.
val index: Int Position in the node's ports list: 0 is the principal port, 1..auxPorts the auxiliaries. Note this is the raw index, one more than the number Node.aux takes.
val kind: PortKind Whether this is the one reactive port or an inert auxiliary. Fixed when the node is created.
val id: String "<nodeId>:<portIndex>" — the stable string identity Net uses for its adjacency index.
override fun toString(): String The port's id.

Node

class Node internal constructor(val id: String, val type: AgentType, val state: NodeState = NodeState())

One node in a net: an AgentType, a stable id, a fixed list of ports and a mutable NodeState.

Declaration Summary
val id: String Unique within its net and never reused, so a ReductionStep's removed ids stay unambiguous after the fact. Minted as type_n by IdSource unless you named it.
val type: AgentType Which kind of agent this is — what rules match on, and what fixes the port count.
val state: NodeState This node's mutable payload. Net.create copies whatever you pass, so two nodes never share one by accident; a rewrite rule must copy it explicitly.
val ports: List<Port> All of this node's ports, principal first at index 0, then the auxiliaries in order.
val principal: Port This node's one reactive port. Wire two of these together and you have an ActivePair.
fun aux(i: Int): Port Auxiliary port i, numbered from 0 — so aux(0) is ports[1].
override fun toString(): String "<id>:<typeName>", e.g. "dup_4:DUP".

Edge

data class Edge(val a: Port, val b: Port)

An undirected wire between two ports. A port holds at most one edge, so this is a matching, not a graph with multiplicity.

Declaration Summary
fun other(p: Port): Port The port at the far end from p — how you walk from a node to its neighbour.

ActivePair

data class ActivePair(val left: Node, val right: Node)

Two nodes whose principal ports are wired to each other — a redex, the only place a rewrite can happen.

Declaration Summary
fun canonicalKey(): Pair<String, String> The two node ids sorted, so the same pair always keys the same way whichever side it arrived on.

Net

class Net internal constructor(internal val ids: IdSource = IdSource())

The net itself: nodes, the wires between their ports, and — because this is meant to be watched — a position and velocity per node.

Declaration Summary
val nodes: Collection<Node> Every node, in creation order. A live view of the net's own map — do not iterate it while rewriting.
val edges: Collection<Edge> Every wire, in creation order. A live view, so a rewrite mid-iteration will upset it.
fun node(id: String): Node The node with this id.
fun positionOf(node: Node): Vec2 Where the node currently sits. (0, 0) for a node this net has never seen, rather than an exception.
fun velocityOf(node: Node): Vec2 The node's current layout velocity — (0, 0) if it has none yet. Carried across rewrites where a rule asks.
fun setPosition(node: Node, p: Vec2) Places the node. Stores the vector by reference, so mutating the Vec2 you passed moves the node afterwards — pass Vec2(p) if that would surprise you.
fun setVelocity(node: Node, velocity: Vec2) Sets the node's layout velocity, copying the vector. A kick here survives into the next relax.
fun create(type: AgentType, id: String = ids.next(type.name.lowercase()), state: NodeState = NodeState()): Node Adds a node of type to the net and returns it, with no wires attached.
fun remove(node: Node) Deletes the node, disconnecting every wire it had first, and forgets its position and velocity.
fun connected(port: Port): Port? The port on the other end of this port's wire, or null if it is free. The one-step graph walk.
fun connect(a: Port, b: Port) Wires two ports together.
fun disconnect(a: Port, b: Port) Removes the wire between two ports, leaving both free.
fun activePairs(): List<ActivePair> Every place the net can currently react: each edge joining two principal ports, de-duplicated by ActivePair.canonicalKey.
fun reduciblePairs(rules: RuleBook): List<ActivePair> The activePairs some rule in rules covers: the places a Reducer could fire next.
fun isNormal(rules: RuleBook): Boolean Whether no rule in rules matches anywhere in the net — what "reduction has finished" means.
fun incrementAges(dt: Double = 1.0) Adds dt to every node's age. Nothing calls this for you, so age advances only on the clock you choose — per frame, per reduction step, per beat.
fun relax(iterations: Int = 1, config: LayoutConfig = LayoutConfig()) Runs the force-directed layout for iterations steps, moving every node.
fun totalEnergy(): Double The sum of every node's energy — a one-number read on the whole net.
fun averageAge(): Double Mean node age, or 0.0 for an empty net. Rises as the net persists, falls as rewrites replace old nodes with new.
fun toFLines(velocityScale: Double = 18.0): List<FLine> Draws the net: one FLine per edge, and several per node — a filled disc and a stroked ring sized and coloured from its NodeState, a spoke per port, a point per port, a halo when the node is half of an active pair, and a streak when it is moving.
fun summaryHtml(title: String = "Interaction net", maxNodes: Int = 8): String A dark-themed HTML card summarising the net — counts, total energy, the highest-energy nodes and the active pairs waiting to fire — for putting in a box output next to the drawing.
fun toSvg(width: Int = 900, height: Int = 700, padding: Double = 40.0): String The net as a standalone SVG document, on white rather than the dark palette the box output uses — the export for putting a net in a paper or a slide.

NodeRef

class NodeRef internal constructor(val name: String)

A name for a node inside a fragment or a rewrite's right-hand side, standing in for a node that does not exist yet.

Declaration Summary
override fun toString(): String The ref's name, as given.

PortRef

data class PortRef(val node: NodeRef, val portIndex: Int)

A port on a not-yet-created node: a NodeRef plus a raw port index, where 0 is the principal port and the auxiliaries start at 1.

NetFragment

class NetFragment internal constructor(internal val nodeSpecs: LinkedHashMap<NodeRef, NodeSpec> = LinkedHashMap(), internal val pendingInternalConnections: MutableList<Pair<PortRef, PortRef>> = mutableListOf(), internal val publishedPorts: LinkedHashMap<String, PortRef> = LinkedHashMap())

A reusable piece of net — some nodes, the wires between them, and a set of named ports it offers to the outside — that can be stamped into a Net as many times as you like.

Declaration Summary
fun export(name: String, port: PortRef) Publishes a port under a name, so whoever embeds this fragment can wire to it without knowing what is inside.

FragmentBuilder

class FragmentBuilder

The receiver inside a fragment block: declare nodes, wire them with to, and expose the ports that should be visible from outside.

Declaration Summary
fun node(name: String, type: AgentType, at: Vec2 = Vec2(), state: NodeState = NodeState(), impulse: Vec2 = Vec2()): NodeRef Declares a node in the fragment and returns the NodeRef you wire with. Nothing is created yet.
fun principal(ref: NodeRef): PortRef The reactive port of a declared node — the one that can form an active pair.
fun aux(ref: NodeRef, i: Int): PortRef Auxiliary port i of a declared node, numbered from 0.
infix fun PortRef.to(other: PortRef) principal(a) to aux(b, 0) — records a wire between two ports of this fragment.
fun expose(name: String, port: PortRef) Publishes a port under a name for whoever embeds this fragment. A repeated name replaces the earlier one.
fun build(): NetFragment The finished NetFragment. fragment calls this for you.

FragmentEmbed

class FragmentEmbed internal constructor(private val nodeMap: Map<NodeRef, Node>)

The result of embedding a fragment: the mapping from the fragment's NodeRefs to the real Nodes that were just created.

Declaration Summary
operator fun get(ref: NodeRef): Node The real node created for this ref.
val nodes: Collection<Node> Every node this embedding created, in the fragment's declaration order.

NetBuilder

class NetBuilder(private val net: Net)

The receiver inside a net block: create nodes, wire them with to, and stamp in NetFragments.

Declaration Summary
fun node(type: AgentType, id: String? = null, at: Vec2 = Vec2(), state: NodeState = NodeState(), impulse: Vec2 = Vec2()): Node Creates a node in the net at an absolute position and returns it, ready to wire.
infix fun Port.to(other: Port) a.principal to b.principal — wires two ports, immediately. Same rules (and exceptions) as Net.connect.
fun connect(a: Port, b: Port) Wires two ports. The non-infix form of to, for when the expression reads better this way.
fun embed(fragment: NetFragment, at: Vec2 = Vec2()): EmbeddedPorts Stamps a NetFragment into the net at at and returns its exported ports, ready to wire to.

EmbeddedPorts

class EmbeddedPorts internal constructor(private val exported: Map<String, Port>)

The ports an embedded fragment published, looked up by the names it exposed — ports["in"].

Declaration Summary
operator fun get(name: String): Port The real port behind an exposed name. Throws if the fragment did not export it.

PairSignature

data class PairSignature(val left: AgentType, val right: AgentType)

The left-hand side of a rewrite rule: the pair of agent types it fires on.

Declaration Summary
fun matches(a: Node, b: Node): Boolean Whether these two nodes are this signature's pair, in either order.
fun bind(a: Node, b: Node): SignatureBinding? Matches the two nodes against this signature, returning them in the signature's order — or null if they are not this pair.

SignatureBinding

data class SignatureBinding(val first: Node, val second: Node, val swapped: Boolean)

A successful PairSignature match: the two nodes in the signature's own order, plus whether that meant swapping them relative to how the edge was found.

RuleMatch

data class RuleMatch(val rule: RewriteRule, val binding: SignatureBinding)

A rule and the binding it matched with — what RuleBook.findMatch returns, and what the scheduler chooses between.

PairContext

data class PairContext(val first: Node, val second: Node, val firstAuxPeers: List<Port?>, val secondAuxPeers: List<Port?>, val center: Vec2, val radius: Double, val firstVelocity: Vec2, val secondVelocity: Vec2, val axis: Vec2, val normal: Vec2)

Everything a rewrite rule gets told about the collision it is replacing: the two nodes in signature order, what their auxiliary ports were attached to, and the local geometry.

Declaration Summary
val first: Node The node matching the rule signature's left type — ordinal 0 throughout the rule DSL, whichever way the edge happened to be stored. About to be deleted.
val second: Node The node matching the signature's right type — ordinal 1. Also about to be deleted.
val firstAuxPeers: List<Port?> What each of first's auxiliary ports was wired to, in aux order; a null entry means that port was already free. These are the loose ends the rule must account for.
val secondAuxPeers: List<Port?> The same for second's auxiliary ports.
val center: Vec2 Midpoint of the two nodes, and the point replacement nodes are seeded at — which is why a rewrite appears to grow out of the collision rather than at the origin.
val radius: Double Roughly half the pair's separation, floored at 12.0 so a coincident pair still gives replacements somewhere to go. The default scale for branchImpulse.
val firstVelocity: Vec2 first's layout velocity at the moment of the collision, so a replacement can inherit its motion.
val secondVelocity: Vec2 second's layout velocity at the moment of the collision.
val axis: Vec2 Unit vector from first toward second. Falls back to (1, 0) for a coincident pair rather than being NaN.
val normal: Vec2 Unit vector perpendicular to axis. The direction branchImpulse throws branches along.
fun auxPeer(nodeOrdinal: Int, auxIndex: Int): Port? What auxiliary port auxIndex of node nodeOrdinal (0 = first, 1 = second) was wired to — the loose end a rule has to reattach.
fun state(nodeOrdinal: Int): NodeState The live state of one of the two nodes — mutating it mutates the doomed node, which is almost never what you want. Use stateCopy to seed a replacement.
fun stateCopy(nodeOrdinal: Int): NodeState An independent copy of one node's state, safe to hand to a new node and then alter.
fun velocity(nodeOrdinal: Int): Vec2 A copy of one node's layout velocity, so a replacement can inherit its motion rather than appearing at rest.
fun blendedState(weightToFirst: Double = 0.5): NodeState The two states averaged — the state for a rewrite that fuses the pair into one node. See NodeState.blend.
val pairEnergy: Double The pair's combined energy. What the energy-biased PairSelectors rank by, and the default event magnitude.
fun branchImpulse(branch: Int, branches: Int = 2, amount: Double = radius * 0.35): Vec2 A sideways kick for branch branch of branches, so replacement nodes fly apart out of the collision instead of being born on top of each other.

RewriteEvent

data class RewriteEvent(val kind: String, val at: Vec2, val magnitude: Double = 1.0, val data: Map<String, Double> = emptyMap())

Something a rewrite wants the outside world to notice — "flash here", "trigger a note", "spawn a particle" — with a free-text kind, a place, a strength and any extra numbers.

Declaration Summary
val kind: String Free text naming what happened — "beta", "spark", "note". Nothing in this file interprets it, so the vocabulary is entirely yours.
val at: Vec2 Where in layout space it happened. Defaults to the collision centre when emitted from a rule.
val magnitude: Double How much of it there was. Defaults to the pair's combined energy, which makes a decaying rule emit progressively weaker events for free.
val data: Map<String, Double> Any extra named numbers the rule wanted to pass along. Copied on emit, so the map you passed is not retained.

ReductionStep

data class ReductionStep(val ruleName: String, val context: PairContext, val swapped: Boolean, val removedNodeIds: List<String>, val createdNodeIds: List<String>, val events: List<RewriteEvent>)

The record of one rewrite that happened: which rule fired, on what, what it removed, what it created and what it emitted.

Declaration Summary
val ruleName: String The RewriteRule.name that fired — which is why it is worth naming rules something you would read in a log.
val context: PairContext The collision as the rule saw it, including the geometry, so a caller can flash the right place on screen.
val swapped: Boolean Whether the pair had to be exchanged to match the rule's signature order. Informational; the rule's behaviour does not depend on it.
val removedNodeIds: List<String> The two consumed nodes' ids. Ids rather than nodes because those nodes are already gone by the time you read this.
val createdNodeIds: List<String> The ids of the replacement nodes, in the order the rule declared them.
val events: List<RewriteEvent> Everything the rule emitted, snapshot-copied — so this survives even though the nodes behind it do not.

RewriteBuilder

class RewriteBuilder internal constructor(private val ctx: PairContext)

The receiver inside a rule { } body: declare the nodes that replace the active pair, wire them, and say what happens to the loose ends.

Declaration Summary
fun node(name: String, type: AgentType, at: Vec2 = Vec2(), state: NodeState = NodeState(), impulse: Vec2 = Vec2()): NodeRef Declares a replacement node and returns its NodeRef. Nothing is created until the rule is applied.
fun principal(ref: NodeRef): PortRef The principal port of a replacement node — wire two of these together and the new net can react again.
fun aux(ref: NodeRef, i: Int): PortRef Auxiliary port i of a replacement node, numbered from 0.
infix fun PortRef.to(other: PortRef) principal(a) to aux(b, 0) — wires two ports inside the replacement. Applied when the rule fires.
fun reconnect(newPort: PortRef, oldPeer: Port?) Attaches a replacement node's port to one of the loose ends the deleted pair left behind — this is how the surrounding net stays connected.
fun reconnect(newPort: PortRef, nodeOrdinal: Int, auxIndex: Int) The same as the other reconnect, naming the loose end by which node of the pair it belonged to (0 = first, 1 = second) and which of its auxiliary ports — the shorter form, and the one that matches ext(...) in the automatic DSL. Silently does nothing if that port was free.
fun passthrough(a: Port?, b: Port?) Wires two of the pair's loose ends directly to each other, bypassing the deleted nodes entirely — how you write an ERASE or a plain "these two were connected through me, now connect them" rule.
fun emit(kind: String, magnitude: Double = ctx.pairEnergy, at: Vec2 = ctx.center, data: Map<String, Double> = emptyMap()) Records a RewriteEvent for whoever is watching the reduction — a flash, a note, a particle.
fun build(): RewriteResult The assembled RewriteResult. RuleBook.rule calls this for you.

RewriteResult

class RewriteResult internal constructor(internal val fragment: NetFragment = NetFragment(), internal val reconnections: MutableList<Pair<PortRef, Port>> = mutableListOf(), internal val passthroughs: MutableList<Pair<Port, Port>> = mutableListOf(), internal val events: MutableList<RewriteEvent> = mutableListOf())

What a rule body produces: the replacement subgraph, how it attaches to the loose ends, which loose ends were joined to each other, and any events emitted.

RewriteRule

class RewriteRule(val name: String, val signature: PairSignature, val rewrite: (PairContext) -> RewriteResult)

One law of motion: a name, the pair of types it fires on, and a function from the collision to its replacement.

Declaration Summary
val name: String What this law is called. Appears in every ReductionStep it produces, so make it readable.
val signature: PairSignature The pair of agent types this fires on, matched in either order.
val rewrite: (PairContext) -> RewriteResult Called afresh on every firing with that collision's context, so the replacement can depend on energy, geometry and velocity rather than being a fixed template.

AutoEndpoint

sealed interface AutoEndpoint

One end of a wire in the automatic-rewiring DSL: either a port of a node the rule is creating (InternalEndpoint) or one of the loose ends the deleted pair left behind (ExternalEndpoint).

InternalEndpoint

data class InternalEndpoint(val ref: PortRef) : AutoEndpoint

A port of a node this rule is creating. Produced by principal(...) / aux(...) in an AutoRuleBuilder.

ExternalEndpoint

data class ExternalEndpoint(val nodeOrdinal: Int, val auxIndex: Int) : AutoEndpoint

A loose end of the old pair: auxiliary port auxIndex of pair node nodeOrdinal (0 = first, 1 = second).

AutoRuleBuilder

class AutoRuleBuilder internal constructor(private val ctx: PairContext)

The receiver inside an autoRule { } body: declare the replacement nodes and then just say what connects to what, mixing new ports and the old pair's loose ends freely.

Declaration Summary
fun node(name: String, type: AgentType, at: Vec2 = Vec2(), state: NodeState = NodeState(), impulse: Vec2 = Vec2()): NodeRef Declares a replacement node and returns its NodeRef.
fun principal(ref: NodeRef): InternalEndpoint The principal port of a node this rule creates, as an endpoint you can wire with to.
fun aux(ref: NodeRef, i: Int): InternalEndpoint Auxiliary port i (from 0) of a node this rule creates, as a wireable endpoint.
fun ext(nodeOrdinal: Int, auxIndex: Int): ExternalEndpoint One of the old pair's loose ends: auxiliary port auxIndex of pair node nodeOrdinal (0 = first, 1 = second).
infix fun InternalEndpoint.to(other: InternalEndpoint) Wires two ports of the replacement together.
infix fun InternalEndpoint.to(other: ExternalEndpoint) Hands one of the old pair's loose ends to a port of the replacement. Ignored if that port was free.
infix fun ExternalEndpoint.to(other: InternalEndpoint) Hands one of the old pair's loose ends to a port of the replacement, written the other way round.
infix fun ExternalEndpoint.to(other: ExternalEndpoint) Splices two of the old pair's loose ends directly to each other — an erase or short-circuit rule.
fun emit(kind: String, magnitude: Double = ctx.pairEnergy, at: Vec2 = ctx.center, data: Map<String, Double> = emptyMap()) Records a RewriteEvent — at the collision centre and with the pair's energy, unless you say otherwise. Same as RewriteBuilder.emit.
fun build(): RewriteResult Resolves every declared connection into the RewriteResult the reducer applies: internal wires stay internal, anything touching an ext becomes a reconnection, and two exts become a passthrough.

RuleBook

class RuleBook

The table of rewrite laws a net runs under — declare rules into it with rule and autoRule, and it answers "does anything apply to these two nodes".

Declaration Summary
fun rule(name: String, left: AgentType, right: AgentType, rewrite: RewriteBuilder.(PairContext) -> Unit) Declares a rule with the full-control RewriteBuilder body — you say what nodes appear and explicitly reconnect or pass through each of the old pair's loose ends.
fun autoRule(name: String, left: AgentType, right: AgentType, rewrite: AutoRuleBuilder.(PairContext) -> Unit) Declares a rule with the declarative AutoRuleBuilder body — you list the connections and the builder works out which are internal, which reattach the old pair's loose ends and which splice two of them together.
fun find(a: Node, b: Node): RewriteRule? The first rule that fires on these two nodes, or null. findMatch when you also need the orientation.
fun findMatch(a: Node, b: Node): RuleMatch? The first rule matching these two nodes, together with the binding that puts them in the rule's own order.

ActivationOrder

enum class ActivationOrder

The ready-made choices for which of several possible reactions fires next — reduction order as a dial.

Constants: FIRST_FOUND, LEFTMOST_BY_ID, RIGHTMOST_BY_ID, RANDOM, HIGHEST_PAIR_ENERGY, LOWEST_PAIR_ENERGY.

PairCandidate

data class PairCandidate(val pair: ActivePair, val match: RuleMatch, val context: PairContext)

One reaction that could happen this step: the active pair, the rule that matched it, and the fully built PairContext the rule would be given.

Declaration Summary
val rule: RewriteRule The rule that would fire. Shorthand for match.rule.

PairSelector

fun interface PairSelector

The scheduler: given every reaction that could fire, pick one — or return null to stop the reduction.

Declaration Summary
fun select(net: Net, candidates: List<PairCandidate>): PairCandidate? Choose the reaction to fire, or null to stop.

PairSelectors

object PairSelectors

The stock PairSelectors — the schedulers ActivationOrder names, plus two that it does not.

Declaration Summary
val firstFound Takes whichever candidate came first out of the edge set. Cheapest, and the most mechanical-looking.
val leftmostById Prefers the pair containing the alphabetically smallest node id — deterministic, and roughly oldest-first, since ids are minted in creation order.
val rightmostById Prefers the pair containing the alphabetically largest node id — deterministic, and roughly newest-first.
val highestPairEnergy Always fires the hottest pair. Tends to produce avalanches and to keep activity concentrated in one region.
val lowestPairEnergy Always fires the coldest pair. Tends to spread activity out and to clear up dregs before hot spots.
fun random(random: Random = Random.Default): PairSelector Fires a uniformly random candidate — the shimmering, non-repeating schedule.
fun nearest(point: Vec2): PairSelector Fires whichever reaction is closest to point — activity follows the cursor, or a moving attractor.
fun from(order: ActivationOrder): PairSelector The selector for an ActivationOrder. RANDOM yields an unseeded random, so it is not reproducible.

Reducer

class Reducer(private val rules: RuleBook, private val selector: PairSelector = PairSelectors.firstFound, private val layout: LayoutConfig = LayoutConfig(), private val relaxIterations: Int = 8)

The engine: takes a RuleBook and a scheduler, and runs a net forward one reaction at a time.

Declaration Summary
fun step(net: Net): Boolean Fires one reaction. False means nothing fired — the net is in normal form, or no rule covers what is left.
fun stepDetailed(net: Net): ReductionStep? Fires one reaction and reports what happened, or null if nothing could fire.
fun reduce(net: Net, maxSteps: Int = Int.MAX_VALUE, onStep: ((Int, Net) -> Unit)? = null): Int Reduces until nothing can fire or maxSteps have run, returning how many fired.
fun reduceDetailed(net: Net, maxSteps: Int = Int.MAX_VALUE, onStep: ((Int, Net, ReductionStep) -> Unit)? = null): Int reduce, but onStep also receives the ReductionStep — so you can collect the events a whole run emitted rather than just watching the net change.

IdSource

class IdSource(private var counter: Int = 0)

The counter behind every automatic node id: hands out prefix_0, prefix_1, … from one shared counter, so ids are unique across prefixes and their numeric order is global creation order.

Declaration Summary
fun next(prefix: String): String The next id: "${prefix}_$n", advancing the shared counter.
fun peekCounter(): Int The counter's current value without consuming it — how many ids have been handed out. Useful for logging.

Types

object Types

Where agent types come from. A one-function namespace, so Types.agent("PULSE", 2) reads as a declaration.

Declaration Summary
fun agent(name: String, auxPorts: Int): AgentType Declares an agent type with auxPorts auxiliary ports — plus the principal port every node has, so its arity is one more than this.

InteractionStage

class InteractionStage internal constructor(val title: String, lines: List<FLine>, val summary: String)

A captured drawing/summary for a stage in an interaction-net comparison.

Declaration Summary
val title: String
val summary: String
val lines: List<FLine> Copies keep later layout or caller mutations from changing the captured stage.

Lambda

object Lambda

Lambda calculus as an interaction net: the agent types, and rules for beta-reduction with explicit copying and discarding.

Declaration Summary
val APP Application. Its principal port is where the function arrives — that is what meets a LAM to beta-reduce — aux(1) is the argument, and aux(0) is where the result comes back out (the beta rule wires the lambda's body to it).
val LAM Abstraction. aux(0) is the binder (where the argument arrives), aux(1) the body.
val DUP Explicit duplication. aux(0) and aux(1) are the two copies; artistically, a branch or an echo.
val ERASE Explicit deletion — a nullary sink. Artistically, a fade or a drain rather than an instant delete.
val VAR A free variable: a nullary atom with nothing but its principal port.
val ROOT The root of a term — where the answer ends up. A nullary atom, distinguished only by its rules.
val CONST An opaque constant. A nullary atom that DUP copies and ERASE consumes.
const val FAN_LABEL: String The NodeState.scalars key a DUP's or DELIM's Lambdascope level lives under (the name is older than the levels). Two agents of one kind meet and cancel when their levels are equal, and copy each other when they differ; see rules. LambdaTerms.compile builds every fan and delimiter at level 0, and the rules raise levels as agents move.
fun fanLabelOf(node: Node): Double A node's level, or 0.0 for one built without one, so two bare node(DUP)s annihilate.
fun fan(level: Int, energy: Double = 1.0): NodeState A NodeState for a fan (or a delimiter) at level — node(DUP, state = Lambda.fan(3)).
val DELIM A scope delimiter, Lambdascope's one extra agent: principal port outward, toward the context the scope sits in, and aux(0) inward. Beta puts one on the result and one on the argument; LambdaTerms.compile puts one on a variable occurrence for every abstraction between it and its binder.
fun levelOf(node: Node): Double The level of a DUP or DELIM (its FAN_LABEL scalar, 0 when absent).
fun rules(): RuleBook The rule book: Lambdascope (van Oostrom, van de Looij and Zwitserlood, Lambdascope: another optimal implementation of the lambda-calculus, 2004), so sharing is optimal and correct on every term, with the energies as art direction.
fun betaBridgeDemo(): Net A minimal APP/LAM bridge demo.
fun duplicatedConstantDemo(): Net A tiny nonlinear sketch: a single CONST is demanded by two roots through a DUP, so the rewrite system has to copy it. No LAM and no body — betaBridgeDemo is the one with those. This is not a full term encoder, but it gives the rewrite system something resource-sensitive to do.
fun eraseApplicationDemo(): Net Erase an application tree to demonstrate explicit garbage collection.

lambdaterms.kt

λ-terms as a front end to the interaction nets in interactionnets.kt: text in, a reduced net out, a term back.

The shortest useful thing is LambdaTerms.normalise, which runs parse, compile, reduce and readback in one call:

val run = LambdaTerms.normalise("(\\m n f x. m f (n f x)) (\\f x. f x) (\\f x. f (f x))")
run.printed   // "λa. λb. a (a (a b))", Church three, from add one two
run.steps     // how many reactions it took

Two public names sit behind that. Term is the syntax tree, with Var, Lam and App nested inside it so that Term.Lam("x", Term.Var("x")) reads as what it is and none of the three short names leaks into afield.lang.lambda. LambdaTerms is everything done to one: parsing, printing, compilation to the Lambda agent vocabulary, readback, and the reference evaluator the net's answer is checked against.

Every answer here is computed twice. The net is one route; LambdaTerms.referenceNormalOrder is the other, and reduces by capture-avoiding substitution with no nets and no sharing anywhere in it, so the two routes share no machinery beyond the term type. LambdaTerms.corpus is the set of closed terms both are run over.

The file lands on the Visualization page because a finished run keeps the Net it reduced, and Net.toFLines draws that net with its active pairs emphasised.

src/jvmMain/kotlin/afield/lang/lambda/lambdaterms.kt — read the source

Term

sealed interface Term

A λ-term: a variable, an abstraction, or an application.

Declaration Summary
data class Var(val name: String) : Term A variable occurrence, bound or free. The name is the source name, or one a readback invented.
data class Lam(val param: String, val body: Term) : Term An abstraction: λ``param``. body.
data class App(val fn: Term, val arg: Term) : Term An application of fn to arg. Applications associate to the left, so f a b is App(App(f, a), b).

LambdaTerms

object LambdaTerms

A λ-calculus front end for interactionnets.kt: a Term syntax you can type, a compiler from a term to a Net, a readback from a reduced net to a term, and — the part that makes the rest checkable — an ordinary substitution evaluator to compare the net's answer against.

Declaration Summary
fun v(name: String): Term Term.Var(name), for when parse is more ceremony than the term is worth.
fun lam(vararg params: String, body: Term): Term λ``params``. body, right-nested — lam("x", "y", body = v("x")) is λx. λy. x.
fun app(fn: Term, vararg args: Term): Term fn applied to each of args in turn, left-associated — app(f, a, b) is f a b.
fun format(term: Term): String A term as source text, with the fewest parentheses that still parse back to the same term: λa. λb. a (a b).
class ParseFailure(message: String, val offset: Int) : IllegalArgumentException("$message (at offset $offset)") What parse throws, carrying the character offset it gave up at.
fun parse(source: String): Term Reads a term.
fun deBruijnKey(term: Term): String The term's de Bruijn form as a string: λλ(#1 (#1 #0)) for Church two, with free variables as @name.
fun alphaEquivalent(a: Term, b: Term): Boolean True when two terms differ only in the names of their bound variables. Compare terms with this, not with ==.
class ReductionBudgetExceeded(val steps: Int) What referenceNormalOrder throws when its budget runs out — which, for a diverging term, is the only outcome.
class IntermediateTermTooLarge(val nodes: Int) What referenceNormalOrder throws when the terms it is building get too big, which a β-step budget does not bound: one substitution can nearly double a term, so a reduction can exhaust memory hundreds of steps inside a budget of thousands.
fun referenceNormalOrder(term: Term, maxSteps: Int = 1_000_000, maxWorkNodes: Int = 40_000_000): Term The oracle: term's normal form under leftmost-outermost (normal-order) reduction, by capture-avoiding substitution.
fun substitute(body: Term, name: String, value: Term): Term body[name := value], renaming any binder of body that would otherwise capture a free variable of value.
fun freeVariablesOf(term: Term): Set<String> The names occurring free in term. Empty for a closed term, which is what corpus asserts of every entry.
const val ROOT_ID: String The id compile gives the single ROOT agent, so readback can still find it after the net is rewritten.
class CompiledTerm internal constructor(val net: Net, val freeVariables: Map<String, String>, val fans: Int, val erasers: Int, val freeNames: List<String> = emptyList()) A Term as a net, plus the little the compiler had to remember in order to read it back.
fun compile(term: Term): CompiledTerm Compiles term to a net over the Lambda vocabulary.
fun compileSource(source: String): CompiledTerm compile, on source text — compile("\\x. x x").
fun readback(net: Net, freeVariables: Map<String, String> = emptyMap(), maxNodes: Int = 200_000, maxDepth: Int = 3_000): Term Walks a net back to a term, starting from the Lambda.ROOT agent's principal port.
fun readback(compiled: CompiledTerm, maxNodes: Int = 200_000, maxDepth: Int = 3_000): Term readback, on what compile returned — so the free-variable names come along by themselves, including on the copies dup-var made of one, which carry its FREE_VARIABLE index rather than an id the map knows.
const val FREE_VARIABLE: String The NodeState.scalars key a free variable's CompiledTerm.freeNames index lives under.
val MARKS: Set<String> The five single-glyph marks of readback's table. ?id is the sixth and is matched by prefix.
fun marksOf(term: Term): Set<String> Every readback mark in term, as the glyphs of readback's table — empty exactly when the answer is a term the reducer is prepared to stand behind.
class Run internal constructor(val term: Term, val result: Term, val steps: Int, val budget: Int, val net: Net, val fans: Int) One normalisation: what went in, what came out, how many reactions it took, and the net it took them in.
fun normalise(term: Term, order: ActivationOrder = ActivationOrder.FIRST_FOUND, maxSteps: Int = 200_000, relaxIterations: Int = 0): Run Compile term, reduce it over Lambda.rules until nothing fires, and read the answer back.
fun normalise(source: String, order: ActivationOrder = ActivationOrder.FIRST_FOUND, maxSteps: Int = 200_000, relaxIterations: Int = 0): Run normalise, on source text. The one-liner this file's overview opens with.
fun corpus(): Map<String, String> The named closed terms this front end is checked against, as source text: Church numerals with succ, add, mul and pred, Church booleans with if, the combinators S, K and I, terms whose reduction duplicates a redex so that sharing has something to do, and terms that erase an argument entirely.

PetriAnalysis.kt

What a net can tell you about itself without being played: how long it is, and whether it is about to wedge.

Petri nets come with about fifty years of theory attached, and much of it answers questions a live coder actually has. Two of them are worth the code.

"How long is this?" A T-invariant is a bag of firings that returns the net to the marking it started from, which is precisely a loop, and a net's minimal T-invariants are its natural musical periods. A net built from a five-step ring against a three-step ring has two of them, of five firings and three, and how long the whole net takes to come back round is a separate question: fifteen steps here, which PetriRun.lasso finds by running it. tInvariants answers the structural question from the incidence matrix alone, with no notion of time or of the initial marking; PetriNet.checkLoopIsInvariant asserts that the loop the engine actually ran is itself a T-invariant (a combination of the minimal ones, in the polyrhythm case), so when it fails the engine and the algebra disagree and one of them has a bug.

"Is this about to wedge?" A net that starves is silent, and finding out at the wrong moment is what makes live coding frightening. willDeadlock runs the net forward and tells you before you commit the change. For the stronger question — whether any interleaving can wedge, rather than the one this net's conflict policy chose — see PetriStateSpace.kt.

Limits

  • Invariants are structural. They see arc weights and nothing else: inhibitor and read arcs are invisible to them, because neither changes the marking and the incidence matrix is a record of change. A net whose behaviour turns on an inhibitor arc has invariants that describe a net it is not. usesTestArcs is how you find out, and report says so itself.

  • Recurrence is detected on PetriMarking.signature, which compares colours by equality: a net that swaps one pattern for another has changed, and one that brings the same pattern object round again has recurred. A net that makes a new colour on every firing never recurs.

  • Everything here is bounded. A net with an unbounded place has infinitely many markings and no loop to find; the answer is null rather than a hang.

src/jvmMain/kotlin/afield/simulation/petri/PetriAnalysis.kt — read the source

Used in: Petri nets, Petri notation cheatsheet, Agent transcript gallery

Top-level functions

Declaration Summary
fun PetriNet.incidence(): Array<IntArray> The net's incidence matrix: Cplace`` is the net change in that place's token count when that transition fires once.
fun PetriNet.incidenceTable(): String The incidence matrix as a readable grid, transitions across the top.
fun PetriNet.tInvariants(maxRows: Int = 4_096): List<Map<String, Int>> The net's minimal T-invariants: bags of firings that leave the marking exactly as they found it.
fun PetriNet.pInvariants(maxRows: Int = 4_096): List<Map<String, Int>> The net's minimal P-invariants: weighted sums of token counts that never change, whatever fires.
fun PetriNet.period(maxSteps: Int = 4_096): PatQ? How long the net takes to come back round to where it started, in cycles, or null if it does not within maxSteps.
fun PetriNet.settle(maxSteps: Int = 4_096): PetriLasso? The net's lasso: its run-in, its loop, and the firing counts of one turn. See PetriRun.lasso.
fun PetriNet.checkLoopIsInvariant(maxSteps: Int = 4_096): String? Checks that the loop the net actually runs is a genuine T-invariant — that firing each transition as often as one turn of the loop does really returns the marking to where it was.
fun PetriNet.willDeadlock(within: Int = 4_096): Boolean Does this net stop? The check to run before committing a live edit.
fun PetriNet.deadlockMarking(within: Int = 4_096): PetriMarking? The marking the net stops in, or null if it does not stop within within steps.
fun PetriNet.deadTransitions(within: Int = 2_048): List<String> Transitions that never fire in within steps of the net's own trajectory.
fun PetriNet.report(maxSteps: Int = 4_096, stateBudget: Int = 4_096): String Everything above, in a paragraph — the thing to print at a net while writing it.

Top-level values

Declaration Summary
val PetriNet.usesTestArcs: Boolean True when any arc is a read or inhibitor arc — the ones the incidence matrix cannot see.

PetriNets.kt

Petri nets: places hold tokens, transitions move them, and the whole thing is a clock. This file is the model and the engine, and the types the rest of the layer is a view onto.

It knows nothing about music. The one thing it takes from afield.audio.synth is PatQ, the exact rational Field uses for time everywhere. PetriMusic.kt turns a net into a PatPattern, PetriVisual.kt draws one, PetriNotation.kt parses the #petri text form, and PetriStateSpace.kt and PetriAnalysis.kt answer questions about one.

Read the surface from the outside in. Build a net: places are auto-declared on first mention, so a two-transition loop is three calls.

val net = petri {
    place("kick", tokens = 1)
    t("hit",  dur = cyc(1, 16), emit = "bd") { from("kick"); to("hats") }
    t("back", dur = cyc(1, 16), emit = "sd") { from("hats"); to("kick") }
}

Run it. A PetriRun is a memoised, append-only trajectory: ask it for any instant and it replays deterministically up to there.

net.run().stepsUntil(PatQ.of(4)).flatMap { it.firings }.forEach(::println)

Tokens carry colour, and a colour can be anything: a note number, a sample name, a whole PatPattern. That is what makes a net a song structure rather than a drum machine. Colours are carried through a firing by default, so the pattern held in verse below is the colour of the token that reaches chorus, with no lambda anywhere.

place("verse", pat("bd*4"))          // one token whose colour is a pattern
t("build", dur = cyc(4)) { from("verse"); from("hats"); to("chorus") }

Three timings, and why

  • PetriTiming.STEP — maximal step. Every transition that is enabled at a grid tick fires, all against the marking as it stood at the start of the tick, and produced tokens do not become available until the tick is over. This is the drum-machine semantics, and the same synchronous step BooleanNetworkSequencing.kt sequences. Conflicts (two transitions wanting the same token) are resolved greedily in PetriConflict order.

  • PetriTiming.EVENT — one transition per tick, the first in conflict order. Interleaving semantics; useful when you want a net to walk rather than pulse.

  • PetriTiming.TIMED — firing takes time. A transition consumes its inputs at the instant it starts and produces its outputs duration later, so a token can be in flight, and a transition with dur = cyc(4) holds a four-cycle section open. Time advances to the next instant at which something happens, not on a grid.

In STEP and EVENT, duration does not affect token flow at all; it only sets how long the emitted event lasts. In all three, delay shifts the emitted event and nothing else, so you can nudge a hit late without perturbing the net's logic.

Why the trajectory is reproducible

PetriMusic.kt presents a net as a PatPattern, and a PatPattern has to be a pure function of time: the scheduler queries it cycle by cycle and every combinator in the language is free to query it again, out of order. That only works if the trajectory is reproducible, so

  • tokens are consumed FIFO, oldest first, never "some matching token";
  • PetriConflict.RANDOM hashes (seed, step index, transition name) rather than pulling from a stateful RNG, so the answer does not depend on how many times anything was asked;
  • PetriRun memoises, so replay is not merely reproducible but literally the same objects.

A net whose conflict policy is genuinely non-deterministic cannot be presented as a pattern at all. PetriLive performs one directly instead, rebuilding the run from the current marking whenever you interfere with it.

src/jvmMain/kotlin/afield/simulation/petri/PetriNets.kt — read the source

Used in: Petri nets, A Petri net that throws sparks, Petri notation cheatsheet

Top-level functions

Declaration Summary
fun cyc(n: Int, d: Int = 1): PatQ An exact rational number of cycles — cyc(1, 16) is a sixteenth. The time unit of every net.
fun blackToken(at: PatQ = PatQ.ZERO): PetriToken Shorthand for a black token born at at.
fun PetriNet.enabled(marking: PetriMarking = initial, at: PatQ = PatQ.ZERO): List<String> Which transitions could fire against marking, ignoring conflict — two transitions can both be listed here and still not both fire, because they may want the same token.
fun petri(build: PetriBuilder.() -> Unit): PetriNet Builds a net with the Kotlin DSL. See PetriBuilder.

PetriToken

data class PetriToken(val colour: Any? = null, val born: PatQ = PatQ.ZERO)

One token. colour is the token's value and may be anything at all — null for the classical black token, a String sample name, an Int note, or a whole PatPattern when the net is being used as a song structure.

Declaration Summary
val isBlack: Boolean True for the classical indistinguishable token.
inline fun <reified T> colourAs(): T? The colour cast to T, or null if it is absent or another type.
override fun toString(): String

PetriMarking

class PetriMarking private constructor(private val byPlace: Map<String, SharedTokenList>)

A marking: which tokens are in which place, in arrival order.

Declaration Summary
operator fun get(place: String): List<PetriToken> The tokens in place, oldest first; empty if the place holds none.
fun count(place: String): Int How many tokens are in place.
val occupied: Set<String> Every place that currently holds at least one token.
val size: Int Total tokens across the whole net.
fun colours(place: String): List<Any?> The colours in place, in arrival order — null entries are black tokens.
fun add(place: String, tokens: List<PetriToken>): PetriMarking A copy with tokens appended to the back of place.
fun withPlace(place: String, tokens: List<PetriToken>): PetriMarking A copy with place set to exactly tokens.
fun toMap(): Map<String, List<PetriToken>> The whole marking as a plain map — a defensive copy, safe to keep.
fun toCounts(): Map<String, Int> The marking as counts only, which is what the analysis layer works in.
fun signature(): String A stable identity for recurrence detection: places, and the colours in them, but not the instants those tokens were born.
fun signatureHash(): Long signature as a 64-bit number.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
override fun toString(): String

PetriPlace

data class PetriPlace(val name: String, val capacity: Int? = null)

A place. capacity of null is unbounded; a bounded place disables any transition that would overfill it.

PetriArcKind

enum class PetriArcKind

What an input arc does to the tokens it looks at.

Constants: NORMAL, READ, INHIBIT.

PetriInput

class PetriInput(val place: String, val weight: Int = 1, val kind: PetriArcKind = PetriArcKind.NORMAL, val guard: ((PetriToken) -> Boolean)? = null)

An input arc: what a transition needs from a place, and which tokens count.

Declaration Summary
val place: String
val weight: Int
val kind: PetriArcKind
val guard: ((PetriToken) -> Boolean)?
override fun toString(): String

PetriOutput

class PetriOutput(val place: String, val weight: Int = 1, val produce: ((PetriBinding) -> List<PetriToken>)? = null)

An output arc: how many tokens a transition puts into a place, and what colour they are.

Declaration Summary
val place: String
val weight: Int
val produce: ((PetriBinding) -> List<PetriToken>)?
override fun toString(): String

PetriColourFlow

enum class PetriColourFlow

What colour a transition's output tokens take when an output arc has no explicit produce.

Constants: BLACK, CARRY.

PetriTransition

class PetriTransition(val name: String, val inputs: List<PetriInput> = emptyList(), val outputs: List<PetriOutput> = emptyList(), val duration: PatQ = PatQ.ZERO, val delay: PatQ = PatQ.ZERO, val priority: Int = 0, val weight: Double = 1.0, val emit: Any? = null, val emitOf: ((PetriBinding) -> Any?)? = null, val enabledIf: ((PetriBinding) -> Boolean)? = null, val colourFlow: PetriColourFlow = PetriColourFlow.CARRY)

A transition.

Declaration Summary
val name: String
val inputs: List<PetriInput>
val outputs: List<PetriOutput>
val duration: PatQ
val delay: PatQ
val priority: Int Higher fires first under PetriConflict.PRIORITY.
val weight: Double Relative likelihood under PetriConflict.RANDOM. Must be > 0.
val emit: Any?
val emitOf: ((PetriBinding) -> Any?)?
val enabledIf: ((PetriBinding) -> Boolean)? A final veto once the tokens are chosen — the place for cross-place conditions.
val colourFlow: PetriColourFlow
val consumes: List<PetriInput> The places this transition consumes from — PetriArcKind.NORMAL inputs only.
override fun toString(): String

PetriConflict

enum class PetriConflict

How a maximal step picks between transitions competing for the same tokens.

Constants: ORDER, PRIORITY, ROUND_ROBIN, RANDOM.

PetriTiming

enum class PetriTiming

See the file header: STEP is a drum machine, EVENT walks, TIMED holds sections open.

Constants: STEP, EVENT, TIMED.

PetriNet

class PetriNet(val places: List<PetriPlace>, val transitions: List<PetriTransition>, val initial: PetriMarking = PetriMarking.EMPTY, val timing: PetriTiming = PetriTiming.STEP, val grid: PatQ = PatQ.of(1, 16), val conflict: PetriConflict = PetriConflict.PRIORITY, val seed: Int = 0)

A net: places, transitions, an initial marking, and the semantics under which it runs.

Declaration Summary
val places: List<PetriPlace>
val transitions: List<PetriTransition>
val initial: PetriMarking
val timing: PetriTiming
val grid: PatQ The tick for PetriTiming.STEP and PetriTiming.EVENT, and the idle advance for TIMED.
val conflict: PetriConflict
val seed: Int
fun place(name: String): PetriPlace? The place called name, or null.
fun transition(name: String): PetriTransition? The transition called name, or null.
val placeNames: List<String> The names of every place, in declaration order.
val transitionNames: List<String> The names of every transition, in declaration order.
fun withMarking(marking: PetriMarking): PetriNet
fun withTiming(timing: PetriTiming = this.timing, grid: PatQ = this.grid): PetriNet
fun withConflict(conflict: PetriConflict = this.conflict, seed: Int = this.seed): PetriNet
fun run(from: PetriMarking = initial, startAt: PatQ = PatQ.ZERO, maxUnroll: Int = DEFAULT_MAX_UNROLL, maxTokens: Int = DEFAULT_MAX_TOKENS): PetriRun A fresh memoised trajectory from from (the net's own initial marking by default).
override fun toString(): String

PetriBinding

class PetriBinding(val transition: String, val at: PatQ, val cycle: Long, val consumed: List<PetriToken>, val byPlace: Map<String, List<PetriToken>>)

The tokens a transition is about to consume, and where they came from.

Declaration Summary
val transition: String
val at: PatQ The instant the transition fires.
val cycle: Long The 0-based cycle containing at.
val consumed: List<PetriToken> Tokens about to be consumed, in input-arc declaration order. READ arcs are not included.
val byPlace: Map<String, List<PetriToken>> Every token this binding looked at, keyed by place — including the ones READ arcs only tested.
operator fun get(place: String): List<PetriToken> Tokens taken from place (empty if none).
fun colour(index: Int = 0): Any? The index-th consumed colour, or null.
inline fun <reified T> colourAs(index: Int = 0): T? The index-th consumed colour cast to T, or null.
inline fun <reified T> colours(): List<T> Every consumed colour that is a T, in order — the usual way to reach pattern-valued tokens.
fun token(colour: Any?): PetriToken One token of colour, born now. Sugar for output arcs.
override fun toString(): String

PetriFiring

data class PetriFiring(val transition: String, val at: PatQ, val arc: PatArc, val cycle: Long, val consumed: List<PetriToken>, val produced: List<PetriToken>, val emit: Any? = null, val index: Long = 0)

One transition firing: when it happened, what it moved, and what it sounds.

Declaration Summary
val transition: String
val at: PatQ
val arc: PatArc
val cycle: Long
val consumed: List<PetriToken>
val produced: List<PetriToken>
val emit: Any?
val index: Long Monotone across a whole run — a firing's identity.
override fun toString(): String

PetriStep

data class PetriStep(val index: Long, val at: PatQ, val before: PetriMarking, val after: PetriMarking, val firings: List<PetriFiring>)

One instant of a run: everything that happened at at, and the marking either side of it.

Declaration Summary
val index: Long
val at: PatQ
val before: PetriMarking
val after: PetriMarking
val firings: List<PetriFiring>
val quiet: Boolean True when the marking is unchanged — only possible for a TIMED step that merely completed.
override fun toString(): String

PetriHalt

enum class PetriHalt

Why a run stopped, when it did.

Constants: RUNNING, DEADLOCK, EXHAUSTED, FLOODED.

PetriLasso

data class PetriLasso(val tailSteps: Int, val loopSteps: Int, val startsAt: PatQ, val duration: PatQ, val firingCounts: Map<String, Int>)

A trajectory that has started repeating: a run-in of tailSteps, then a loop of loopSteps lasting duration.

Declaration Summary
val tailSteps: Int How many steps happen once, before the loop.
val loopSteps: Int How many steps the loop is.
val startsAt: PatQ The instant the loop's first step happens at.
val duration: PatQ How long one turn of the loop takes, in cycles. The net's own bar length.
val firingCounts: Map<String, Int> How many times each transition fires per turn — a realised T-invariant.
override fun toString(): String

PetriRun

class PetriRun internal constructor(val net: PetriNet, val from: PetriMarking, val startAt: PatQ = PatQ.ZERO, val maxUnroll: Int = PetriNet.DEFAULT_MAX_UNROLL, val maxTokens: Int = PetriNet.DEFAULT_MAX_TOKENS)

A deterministic, memoised, append-only trajectory through a net.

Declaration Summary
val net: PetriNet
val from: PetriMarking
val startAt: PatQ
val maxUnroll: Int
val maxTokens: Int
val halt: PetriHalt Why the run stopped, or PetriHalt.RUNNING if it has not. Only meaningful after a query.
val floodedPlace: String? The place that passed maxTokens, when that is why the run stopped, and null otherwise.
val materialised: Int How many steps have been materialised so far.
val reach: PatQ The instant the trajectory currently reaches.
fun steps(count: Int): List<PetriStep> The first count steps, extending the trajectory if needed. Shorter if the net halts.
fun stepsUntil(instant: PatQ): List<PetriStep> Every step strictly before instant.
fun stepsIn(from: PatQ, until: PatQ): List<PetriStep> Every step whose instant lies in [from, until).
fun firingsOver(arc: PatArc): List<PetriFiring> Every firing whose event arc intersects arc.
fun markingAt(instant: PatQ): PetriMarking The marking as it stood immediately before instant.
fun countAt(place: String, instant: PatQ): Int How many tokens were in place at instant. The raw material for a control signal.
fun changes(place: String, until: PatQ): List<Pair<PatQ, Int>> Every instant at which place's token count changed, up to until, with the count it changed to. The first entry is always startAt and the initial count, so the list is total.
fun lasso(maxSteps: Int = 4_096): PetriLasso? Where the run settles: how long before it starts repeating, and how long the repeat is.
fun reset() Drop every materialised step and start again from the initial marking.
override fun toString(): String

PetriBuilder

class PetriBuilder internal constructor()

Builds a net. Places are auto-declared the first time a transition mentions them, so you only write place when you want an initial marking, a capacity, or coloured tokens.

Declaration Summary
var timing: PetriTiming
var grid: PatQ
var conflict: PetriConflict
var seed: Int
fun place(name: String, tokens: Int = 0, capacity: Int? = null, colours: List<Any?> = emptyList(), at: PatQ = PatQ.ZERO): String Declares a place.
fun place(name: String, vararg colours: Any?): String Declares a place holding exactly these coloured tokens — place("verse", pat("bd*4")).
fun t(name: String, dur: PatQ = PatQ.ZERO, delay: PatQ = PatQ.ZERO, priority: Int = 0, weight: Double = 1.0, emit: Any? = null, colourFlow: PetriColourFlow = PetriColourFlow.CARRY, build: PetriTransitionBuilder.() -> Unit = {}): String Declares a transition. The block wires its arcs.

PetriTransitionBuilder

class PetriTransitionBuilder internal constructor(private val name: String)

Wires one transition's arcs. See PetriBuilder.t.

Declaration Summary
fun from(place: String, weight: Int = 1, guard: ((PetriToken) -> Boolean)? = null) Consume weight tokens from place, optionally only those matching guard.
fun read(place: String, weight: Int = 1, guard: ((PetriToken) -> Boolean)? = null) Require weight tokens in place but consume none — a test arc, &p in the notation.
fun inhibit(place: String, weight: Int = 1, guard: ((PetriToken) -> Boolean)? = null) Require fewer than weight tokens in place — an inhibitor arc, ~p in the notation.
fun to(place: String, weight: Int = 1, produce: ((PetriBinding) -> List<PetriToken>)? = null) Produce weight tokens into place. Without produce the transition's PetriColourFlow decides their colour — by default the consumed colours are carried through.
fun toColoured(place: String, colour: (PetriBinding) -> Any?) Produce one token into place whose colour is computed from the binding.
fun emit(of: (PetriBinding) -> Any?) What this firing sounds, when it depends on which tokens were consumed.
fun onlyIf(predicate: (PetriBinding) -> Boolean) A final veto once the tokens are chosen.
override fun toString(): String

PetriNotation.kt

The #petri text notation: a net you can retype while it is playing.

The Kotlin DSL in PetriNets.kt is the complete surface; anything you can express, you can express there, including arbitrary lambdas. This file is the terse surface. Live coding is editing, and editing a net is only pleasant when the whole net fits in view and one arc is one line. For players, the whole notation is summarised on the Petri notation cheatsheet.

#petri
!grid 1/16

kick : 1
kick -[ hit  @1/16 : bd ]-> hats
hats -[ back @1/16 : sd ]-> kick

petri turns that text into a PetriNet; petriParse returns the net along with everything it has to say about the source.

One line, one arc

pre -[ name @dur key=value : emit => combinator ]-> post

Both sides are +-joined lists of weight*place, and either may be empty: -[ spawn ]-> p is a source and p -[ sink ]-> is a drain. A place is created the first time it is mentioned. A transition may be named on more than one line, which is how one with several inputs stays readable; its settings belong on the first of those lines, and a later line's are ignored with a warning.

verse  -[ build ]-> chorus
energy -[ build ]-> chorus

Prefixes on an input mark the two arcs that are not ordinary.

~armed  -[ fill ]-> hats        // inhibitor: only while armed is empty
&clock  -[ tick ]-> bar         // read/test: needs a token, consumes none
2*hats  -[ roll ]-> fill        // weight: needs two

Place declarations

kick   : 1              // one black token
dst    : 0 ^4           // empty, and bounded at four
verse  : "bd ~ sd ~"    // ONE token whose colour is that pattern — a section
label  : 'lead'         // one token coloured with the literal string
notes  : #60, #64, #67  // three numeric tokens

The two kinds of quote differ: a double-quoted string is parsed as a pattern and a single-quoted one stays a string, because a place holding a rhythm is what makes a net a song structure rather than a drum machine.

Directives

!timing step | event | timed
!grid 1/16
!conflict order | priority | round-robin | random
!seed 7

Everything else

// starts a comment. A leading #petri line is the editor's marker and is ignored here. Colour transforms after => come from PetriCombinators, a fixed vocabulary rather than compiled Kotlin, which keeps the notation quick to parse and the cheatsheet finite. When you want a real lambda, that is what the Kotlin DSL is for.

Errors are collected rather than thrown at the first one: petriParse hands back everything it found, so an editor can underline all of it at once. petri is the throwing convenience on top.

src/jvmMain/kotlin/afield/simulation/petri/PetriNotation.kt — read the source

Top-level functions

Declaration Summary
fun petri(source: String): PetriNet Builds a net from the #petri notation, throwing on the first error. See petriParse.
fun petriParse(source: String): PetriParseResult Parses the #petri notation, collecting every complaint rather than stopping at the first.

PetriSpan

data class PetriSpan(val start: Int, val end: Int, val line: Int, val column: Int, val text: String)

Where something is in the source. Offsets are into the whole string; line and column are 1-based.

Declaration Summary
override fun toString(): String

PetriSeverity

enum class PetriSeverity

Constants: ERROR, WARNING.

PetriDiagnostic

data class PetriDiagnostic(val message: String, val span: PetriSpan, val severity: PetriSeverity = PetriSeverity.ERROR)

One complaint about a source, with the span to underline.

Declaration Summary
override fun toString(): String

PetriParseResult

class PetriParseResult(val net: PetriNet?, val diagnostics: List<PetriDiagnostic>, val placeSpans: Map<String, PetriSpan> = emptyMap(), val transitionSpans: Map<String, PetriSpan> = emptyMap())

What petriParse found: the net if it could build one, and everything it wants to say.

Declaration Summary
val net: PetriNet?
val diagnostics: List<PetriDiagnostic>
val placeSpans: Map<String, PetriSpan>
val transitionSpans: Map<String, PetriSpan>
val errors: List<PetriDiagnostic>
val warnings: List<PetriDiagnostic>
val ok: Boolean
override fun toString(): String

PetriCombinators

object PetriCombinators

The named colour transforms the notation's => can reach.

Declaration Summary
val names: List<String> Every combinator name, in the order the cheatsheet lists them.
operator fun get(name: String): ((PetriBinding) -> Any?)? The transform called name, or null if there is no such word.
val docs: Map<String, String> A one-line description of each word, for editor hovers and the cheatsheet.

PetriStateSpace.kt

Every marking a net can reach by any interleaving, and the classical properties read off that: boundedness, safety, deadlock, quasi-liveness, liveness, reversibility, home markings, mutual exclusion. Exactly, and with a counterexample when the answer is no.

PetriAnalysis.kt asks the deterministic question instead: willDeadlock runs the net forward under its own PetriConflict policy and reports whether that one interleaving wedged, and deadTransitions calls a transition dead when it did not fire in that one run. For a live coder that is usually the useful question, because the conflict policy is part of the instrument.

It is not the question the textbook asks, and not the one a five-philosopher table asks either. A symmetric dining table deadlocks under some interleavings and not others, so running it once and finding it still going proves nothing; you have to enumerate. That example is worked through under Checking every possible order.

A deadlock the search found is real whether or not the budget ran out: it came with a firing sequence, and PetriTrace.verify re-fires that sequence through the engine to prove it. But "there is no deadlock" is a claim about markings nobody visited, so every accessor that would make such a claim returns null, meaning unknown, when the search was truncated (PetriSearchOutcome.BUDGET rather than PetriSearchOutcome.COMPLETE). Nothing here reports a truncated search as safe.

What this sees that the incidence matrix cannot

The successor relation is PetriNet.fire, which calls the engine's own binding step. So read arcs, inhibitor arcs, colour guards, enabledIf and place capacities are all handled: an explicit search asks "is this transition enabled here?" and gets the real answer, where tInvariants and pInvariants ask the incidence matrix, which is a record of change and so blind to every arc that does not move a token. A net whose mutual exclusion is enforced by an inhibitor arc has no P-invariant that proves it, and a state space that does.

The price is the classical one: reachability for a net with inhibitor arcs is undecidable in general, so this may exhaust its budget where the algebra would have answered instantly. The two layers are complements, not replacements.

Two searches, and when to use which

  • PetriNet.stateSpace is exact and handles every feature of the model, but only terminates on a bounded net. An unbounded one exhausts the budget and says so.
  • PetriNet.coverability is Karp–Miller: it accelerates growth to ω and therefore always terminates, which is how you get per-place boundedness for a net whose state space is infinite. Karp–Miller is only valid for plain place/transition nets, though. Inhibitor arcs, place capacities and colour-dependent behaviour each break the acceleration argument, and each adds an entry to PetriCoverabilityGraph.caveats rather than being dropped silently.

The state key

PetriMarking.signature is the obvious key and the wrong one here: it sorts the colours within a place, so it loses arrival order, and consumption is FIFO, so two markings holding the same multiset in a different order have genuinely different futures.

That makes the key coarser than the state, and a coarse key in a reachability search merges states whose futures differ, which can hide a reachable deadlock behind an already-visited marking and call the net safe. An unsound safe is the error that matters, so the key here is strict: places, and the colours in them in arrival order, compared by the colours' own equals.

The cost lands in the survivable direction. A net that mints a fresh colour object per firing has an effectively infinite state space and exhausts the budget, and PetriSearchOutcome.BUDGET reports that rather than guessing.

PetriToken.born is not in the key. The search fires everything at PatQ.ZERO — these are the untimed semantics, with timing, grid and conflict policy all discarded — so born never varies during a search anyway. The residual exposure is a guard or an enabledIf that reads born, which the state space evaluates at zero and a real run does not.

src/jvmMain/kotlin/afield/simulation/petri/PetriStateSpace.kt — read the source

Used in: Petri nets, A Deadlock, Sculpted and Traced, Petri notation cheatsheet

Top-level functions

Declaration Summary
fun PetriNet.fire(marking: PetriMarking, transition: String): PetriMarking? The marking after transition fires alone against marking, or null if it cannot fire there.
fun PetriNet.successors(marking: PetriMarking): List<Pair<String, PetriMarking>> Every marking reachable from marking in one firing, with the transition that reaches it.
fun PetriNet.stateSpace(budget: Int = PETRI_STATE_BUDGET, reduction: PetriReduction = PetriReduction.NONE): PetriStateGraph Every marking reachable from PetriNet.initial under every interleaving, breadth-first, stopping at budget markings.
fun PetriNet.coverability(budget: Int = PETRI_STATE_BUDGET): PetriCoverabilityGraph The Karp–Miller coverability graph: a finite over-approximation of the state space in which a place that can grow without bound is marked ω.
fun PetriNet.stateSpaceReport(budget: Int = PETRI_STATE_BUDGET): String The classical analysis in a paragraph, alongside the deterministic one from PetriNet.report.

Top-level values

Declaration Summary
const val PETRI_STATE_BUDGET: Int How many markings a search will hold before it gives up and says so.
const val PETRI_OMEGA: Int The ω of a coverability graph: "this place grows without bound".
val PetriNet.isColourDependent: Boolean True when anything about this net's behaviour depends on what colour its tokens are — a guard, an enabledIf, an explicit produce, or a coloured token in the initial marking.

PetriSearchOutcome

enum class PetriSearchOutcome

Whether a search saw the whole space, and therefore whether its negative answers mean anything.

Constants: COMPLETE, BUDGET.

PetriStateEdge

data class PetriStateEdge(val from: Int, val transition: String, val to: Int)

One labelled edge of a state graph: firing transition in state from reaches state to.

PetriTrace

class PetriTrace(val net: PetriNet, val transitions: List<String>, val markings: List<PetriMarking>)

A firing sequence and the markings it passes through — the deliverable of this whole file.

Declaration Summary
val net: PetriNet
val transitions: List<String>
val markings: List<PetriMarking>
val length: Int How many firings. A shortest counterexample's length is a number worth asserting.
val start: PetriMarking Where the trace starts — the initial marking, for anything this file produces.
val end: PetriMarking Where it ends. For a deadlock trace, the marking that enables nothing.
fun verify(): String? Re-fires the sequence from start and checks it lands where claimed, returning null when it does and a description of the first discrepancy when it does not.
override fun toString(): String The firing sequence on one line — grab.0, grab.1, grab.2 — or (nothing) when empty.
fun describe(ending: String? = null): String The trace with a marking under every firing, for reading rather than for a log line.

PetriStateGraph

class PetriStateGraph internal constructor(val net: PetriNet, val states: List<PetriMarking>, val edges: List<PetriStateEdge>, val outcome: PetriSearchOutcome, val budget: Int, val reduction: PetriReduction, private val parent: IntArray, private val parentVia: Array<String?>, private val expanded: BooleanArray, val caveats: List<String> = emptyList())

Every marking reachable from the net's initial marking, under every interleaving, and the labelled edges between them.

Declaration Summary
val net: PetriNet
val states: List<PetriMarking>
val edges: List<PetriStateEdge>
val outcome: PetriSearchOutcome
val budget: Int
val reduction: PetriReduction Whether every interleaving was explored, or a deadlock-sufficient subset of them.
val caveats: List<String> What the search did other than what was asked, in a sentence each: today, a PetriReduction.STUBBORN request on a colour-dependent net, which is searched unreduced. Empty when it did exactly what was asked. report prints these.
val stateCount: Int How many distinct markings were found. Exact when complete.
val edgeCount: Int How many labelled firings connect them.
val complete: Boolean True when the whole space was visited, which is what every negative answer here depends on.
val exact: Boolean True when this graph is the whole reachable state space, and therefore when a property other than deadlock can be read off it.
fun depth(id: Int): Int How many firings from the initial marking to state id — its BFS layer.
val diameterFound: Int The deepest BFS layer in the graph — how far the search got.
fun pathTo(id: Int): PetriTrace The shortest firing sequence from the initial marking to state id.
val deadMarkings: List<Int> States that were fully expanded and enable nothing — the reachable deadlocks.
fun deadlockTrace(): PetriTrace? The shortest firing sequence from the initial marking to a reachable deadlock, or null if there is none — which only means "there is none" when complete.
val canDeadlock: Boolean? True when some interleaving wedges, false when none can, null when the search was truncated and the honest answer is that nobody knows.
fun observedBound(place: String): Int The largest token count seen in place anywhere in the graph. A lower bound, always.
fun bound(place: String): Int? The exact bound on place — its maximum over all reachable markings — or null if unknown.
val bounds: Map<String, Int>? Every place's exact bound, or null when the search was truncated or reduced.
val isSafe: Boolean? True when no place ever holds more than one token — the net is 1-bounded. Null if unknown.
val quasiLive: Set<String> Transitions that fire in some reachable marking — the honest version of PetriNet.deadTransitions, which reports the ones that did not fire in one particular run.
fun neverFires(): List<String>? Transitions that can never fire under any interleaving, or null when the search was truncated — because a transition nobody reached might be waiting in a marking nobody visited.
val liveTransitions: Set<String>? Transitions that stay possible forever: from every reachable marking, some continuation fires them. Null when the search was truncated.
val isLive: Boolean? True when every transition is live — the classical L4-liveness. Null when unknown.
val isReversible: Boolean? True when the initial marking is reachable from every reachable marking: the net can always be put back. Null when the search was truncated.
val homeMarkings: List<Int>? Markings reachable from every reachable marking — the states the net can always get back to.
fun witness(predicate: (PetriMarking) -> Boolean): PetriTrace? The shortest firing sequence to a reachable marking satisfying predicate, or null if none was found — proof that none exists only when complete.
fun invariant(predicate: (PetriMarking) -> Boolean): PetriTrace? The shortest firing sequence that breaks predicate, or null when nothing does — which is a proof of the invariant when complete.
fun proves(predicate: (PetriMarking) -> Boolean): Boolean? True when predicate holds in every reachable marking, false with a counterexample available from invariant, and null when neither can be claimed — the search was truncated or reduced.
fun mutualExclusionFailure(vararg places: String): PetriTrace? The shortest firing sequence that puts tokens in two or more of places at once, or null when no reachable marking does.
fun mutuallyExclusive(vararg places: String): Boolean? True when places never hold tokens simultaneously; null when the search was truncated.
fun report(): String The classical properties in a paragraph — what to print at a net you are about to trust.
override fun toString(): String

PetriOmegaMarking

class PetriOmegaMarking internal constructor(internal val counts: Map<String, Int>)

A marking in which a place may hold ω tokens — arbitrarily many, as many as you like.

Declaration Summary
operator fun get(place: String): Int Tokens in place, or PETRI_OMEGA.
fun isOmega(place: String): Boolean True when place holds ω.
val omegaPlaces: Set<String> Every place holding ω here.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
override fun toString(): String

PetriCoverabilityGraph

class PetriCoverabilityGraph internal constructor(val net: PetriNet, val nodes: List<PetriOmegaMarking>, val edges: List<PetriStateEdge>, val outcome: PetriSearchOutcome, val caveats: List<String>)

A Karp–Miller coverability graph: finite for every net, at the cost of only being exact about covering rather than about reachability.

Declaration Summary
val net: PetriNet
val nodes: List<PetriOmegaMarking>
val edges: List<PetriStateEdge>
val outcome: PetriSearchOutcome
val caveats: List<String>
val sound: Boolean True when Karp–Miller's assumptions actually hold for this net. See the class KDoc.
val unboundedPlaces: List<String> Places that can hold arbitrarily many tokens.
val isBounded: Boolean True when no place is unbounded.
fun bound(place: String): Int? The bound on place: the largest number of tokens it can hold, or null when it is unbounded.
fun report(): String One paragraph: how big the graph is, what grows, and whether to believe any of it.
override fun toString(): String

PetriReduction

enum class PetriReduction

Whether the search explores every interleaving or a sufficient subset of them.

Constants: NONE, STUBBORN.

PetriVisual.kt

Draws a Petri net as an FLine picture, and lets you click it to add tokens and fire transitions.

A Petri net has a standard picture: circles hold tokens, bars move them, arrows say where. The two things you most need to see while debugging a net are which transitions are ready to go and where the tokens are, so the drawing shows four things the plain graph does not:

  • a place is a circle with its tokens as dots inside it, and a token carrying a PatPattern gets a ring rather than a dot, because a place holding a rhythm is a different kind of thing from a place holding a count;
  • a transition is a bar, drawn dim when it cannot fire, bright when it is enabled, and filled when it is firing at the instant being drawn;
  • an inhibitor arc ends in a small circle and a read arc in no head at all, the standard notation for a distinction a token count cannot show;
  • arc weights above one are written on the arc.

Interaction goes through FLine.onClick, so it only works in the interactive outputs. Click a place to drop a token in, click a transition to make it fire. Both need somewhere for the change to go, so both are wired to a PetriLive; a PetriRun is an immutable trajectory, and clicking one only reports on it.

val live = net.live()
box.showPetriNet(live)          // click the picture; the net changes under your hands

Layout

PetriLayout.RING draws a cycle as a cycle, which is what most drum nets are, and is chosen automatically when the net is one. Otherwise PetriLayout.FORCE relaxes a spring model. PetriLayout.TALA hands the net to d2's diagram layout engine instead, if you have built the optional helper for it; it falls back to PetriLayout.FORCE if you have not. PetriDrawing.at pins anything you would rather place yourself.

src/jvmMain/kotlin/afield/simulation/petri/PetriVisual.kt — read the source

Used in: Petri nets, Petri notation cheatsheet

Top-level functions

Declaration Summary
fun PetriRun.frameAt(instant: PatQ = PatQ.ZERO, layout: PetriLayout = PetriLayout.AUTO, radius: Double = 240.0): PetriDrawing The frame a run is showing at instant: the marking then, what was ready, and what is sounding.
fun PetriNet.frame(layout: PetriLayout = PetriLayout.AUTO, radius: Double = 240.0): PetriDrawing The frame for a net sitting at its initial marking — the picture before anything has happened.
fun clearPetriLayoutCache() Drops every cached layout, so the next frame re-solves.
fun PetriDrawing.arcCrossings(): Int How many of this drawing's arcs cross each other.
fun PetriDrawing.toFLines(labels: Boolean = true, labelSize: Double = 15.0, labelLimit: Int = PETRI_LABEL_LIMIT): List<FLine> The picture — hand it to lines(...) or use Box.showPetriNet.
fun petriLabelsRenderable(): Boolean Whether this JVM can turn a label into glyph outlines at all.
fun Box.showPetriNet(drawing: PetriDrawing, channel: String = "petri", summaryChannel: String? = "$channel-summary", w: Double = 900.0, h: Double = 560.0, labels: Boolean = true, fitToSize: Boolean = true): List<FLine> Draws a static frame of a net into a box output.
fun Box.showPetriNet(net: PetriNet, channel: String = "petri", summaryChannel: String? = "$channel-summary", w: Double = 900.0, h: Double = 560.0, layout: PetriLayout = PetriLayout.AUTO, labels: Boolean = true): List<FLine> showPetriNet for a net sitting at its initial marking.
fun Box.showPetriNet(run: PetriRun, at: PatQ, channel: String = "petri", summaryChannel: String? = "$channel-summary", w: Double = 900.0, h: Double = 560.0, layout: PetriLayout = PetriLayout.AUTO, labels: Boolean = true): List<FLine> showPetriNet for a run at a particular instant — the frame the animation harness draws.
fun Box.showPetriNet(live: PetriLive, channel: String = "petri", summaryChannel: String? = "$channel-summary", w: Double = 900.0, h: Double = 560.0, layout: PetriLayout = PetriLayout.AUTO, labels: Boolean = true, onChange: (() -> Unit)? = null): List<FLine> Draws a live net, and wires the picture up so clicking it changes the net.
fun PetriDrawing.toSvg(w: Double = 900.0, h: Double = 560.0, labels: Boolean = true): String The drawing as a standalone SVG string — for docs, for a file, for anywhere without a box.
fun PetriDrawing.summaryHtml(title: String = "Petri net", live: Boolean = false): String A dark card summarising the frame: the marking, what is ready, what is sounding, and the period.
fun PetriMarking.asciiRow(net: PetriNet): String An ASCII picture of the marking, for a terminal, a log, or a smoke test.
fun PetriNet.asciiTimeline(steps: Int = 16): String PetriRun.asciiTimeline from a fresh run — what you want when you are looking at a net, not a performance.
fun PetriRun.asciiTimeline(steps: Int = 16): String A marking-over-time strip: one row per step, one column per place. The cheapest useful picture.
fun PetriStateGraph.tooDenseToDraw(maxNodes: Int = PETRI_STATE_DRAW_LIMIT): Boolean True when this graph has more markings than maxNodes, and so toFLines will summarise rather than draw it.
fun PetriStateGraph.toFLines(highlight: PetriTrace? = deadlockTrace(), w: Double = 1200.0, h: Double = 700.0, labels: Boolean = true, labelSize: Double = 13.0, maxNodes: Int = PETRI_STATE_DRAW_LIMIT): List<FLine> The state graph as a picture: BFS layers left to right, one dot per reachable marking, and the shortest route to a deadlock drawn over the top of them.
fun PetriStateGraph.toSvg(highlight: PetriTrace? = deadlockTrace(), w: Double = 1200.0, h: Double = 700.0, labels: Boolean = true, maxNodes: Int = PETRI_STATE_DRAW_LIMIT): String The drawing as a standalone SVG string — for docs, for a file, for anywhere without a box.
fun Box.showPetriStateSpace(graph: PetriStateGraph, highlight: PetriTrace? = graph.deadlockTrace(), channel: String = "statespace", summaryChannel: String? = "$channel-summary", w: Double = 1200.0, h: Double = 700.0, labels: Boolean = true, maxNodes: Int = PETRI_STATE_DRAW_LIMIT): List<FLine> Draws the state space into a box, with the classical properties as a card beside it.
fun Box.showPetriStateSpace(net: PetriNet, channel: String = "statespace", summaryChannel: String? = "$channel-summary", w: Double = 1200.0, h: Double = 700.0, budget: Int = PETRI_STATE_BUDGET, maxNodes: Int = PETRI_STATE_DRAW_LIMIT): List<FLine> showPetriStateSpace for a net, searched at the default budget.
fun PetriStateGraph.summaryHtml(highlight: PetriTrace? = deadlockTrace(), maxNodes: Int = PETRI_STATE_DRAW_LIMIT): String The card beside the picture: how big the space is, what the search proved, and — when there is one — the counterexample written out as a firing sequence.

Top-level values

Declaration Summary
const val PETRI_LABEL_LIMIT Default label budget for toFLines: how many places-plus-transitions can carry names.
const val PETRI_STATE_DRAW_LIMIT: Int How many markings PetriStateGraph.toFLines will draw one dot each for before it declines.

PetriLayout

enum class PetriLayout

How frame and Box.showPetriNet arrange places and transitions on the page.

Constants: AUTO, RING, FORCE, LAYERED, TALA.

PetriDrawing

class PetriDrawing(val net: PetriNet, val marking: PetriMarking, val enabled: Set<String> = emptySet(), val firing: Set<String> = emptySet(), val at: PatQ = PatQ.ZERO, layout: PetriLayout = PetriLayout.AUTO, pinned: Map<String, Vec2> = emptyMap(), val radius: Double = 240.0)

One frame: a net, a marking, what is ready, what is sounding, and where everything sits.

Declaration Summary
val net: PetriNet
val marking: PetriMarking
val enabled: Set<String> Transitions that could fire against marking. Drawn bright.
val firing: Set<String> Transitions whose event arc covers the instant being drawn. Drawn filled.
val at: PatQ The instant this frame shows, for the caption.
val radius: Double
val positions: Map<String, Vec2> Where each place and transition sits, in the drawing's own coordinates.
fun at(name: String, x: Number, y: Number): PetriDrawing A copy with name pinned at (x, y).
fun position(name: String): Vec2
override fun toString(): String

ControlLaw.kt

The mapping between a slider's travel and its value: linear, logarithmic, power-taper or bipolar.

ControlLaw names the law and its wire spelling, ControlLawSpec pairs it with a range and maps travel to value and back, and controlLawDecimals picks how many decimals a readout shows. The slider(...) builder in the output controls takes a law argument of this type.

src/jvmMain/kotlin/afield/ui/controls/ControlLaw.kt — read the source

Top-level functions

Declaration Summary
fun controlLawDecimals(value: Double, significantDigits: Int, stepDecimals: Int): Int How many decimals to show for value, given a significant-digit budget.

ControlLaw

enum class ControlLaw(val wireName: String)

How a control's travel maps onto its value.

Constants: LINEAR, LOGARITHMIC, EXPONENTIAL, BIPOLAR.

Declaration Summary
val wireName: String The spelling that travels to the browser in ControllerControl.law.

ControlLawSpec

data class ControlLawSpec(val law: ControlLaw = ControlLaw.LINEAR, val min: Double = 0.0, val max: Double = 1.0, val exponent: Double = 2.0, val center: Double = (min + max) / 2.0)

A law together with the range it applies to — everything needed to map travel to value and back.

Declaration Summary
val law: ControlLaw
val min: Double
val max: Double
val exponent: Double
val center: Double
fun validate(label: String) Rejects a range this law cannot express, naming label so the message points at the control.
fun valueAt(t: Double): Double The value at travel t, where t is a fraction of the track.
fun normalizedFor(value: Double): Double The travel that shows value — the inverse of valueAt, clamped to 0.0..1.0.

canvasFlight.kt

Filming Field: canvasFlight flies the canvas through a document's bookmarks, live or to an MP4.

The flight opens on the first bookmark and holds, then flies to each next one and holds there. Each leg eases in and out, zooming in log space while the point at the centre of the window travels in a straight line across the canvas. Every three.js view showing on the canvas turns slowly about its own target meanwhile, and gets its camera back afterwards.

Live, the flight runs in every window open on the document, on the browser's own clock. Filmed (record = "flight.mp4"), it is rendered frame by frame in the capture page and encoded with ffmpeg: the clock waits for each frame, so the film is smooth however long a frame takes.

The arithmetic is afield.app.canvas.CanvasFlightTimeline; the browser side is CanvasFlightClient; the recorder is afield.tools.docs.recordCanvasFlight. Docs: public/docs/filming_the_canvas.md.

src/jvmMain/kotlin/afield/ui/film/canvasFlight.kt — read the source

Used in: Filming the canvas

Top-level functions

Declaration Summary
fun Box.canvasFlight(bookmarks: List<String>, legSeconds: Double = 6.0, holdSeconds: Double = 2.0, orbitDegreesPerSecond: Double = 8.0, pullBack: Boolean = true, record: String? = null, fps: Int = 30, width: Int? = 1920, height: Int? = 1080, pixelRatio: Double = 1.5): CanvasFlight Flies the canvas through bookmarks — names as the bookmarks panel shows them, or slots as "1".."9" — spending legSeconds flying to each and holdSeconds holding at each, the first included. Every visible three.js view turns orbitDegreesPerSecond about its own target meanwhile (0 keeps them still). A leg whose straight path would cross empty canvas zooms out on the way unless pullBack is false.
fun Box.canvasFlight(vararg stops: FlightStop, legSeconds: Double = 6.0, holdSeconds: Double = 2.0, orbitDegreesPerSecond: Double = 8.0, pullBack: Boolean = true, record: String? = null, fps: Int = 30, width: Int? = 1920, height: Int? = 1080, pixelRatio: Double = 1.5): CanvasFlight canvasFlight with a leg and hold per stop: FlightStop("plan", legSeconds = 4.0, holdSeconds = 3.0). A stop that leaves them null takes legSeconds and holdSeconds.
fun Box.stopCanvasFlight() Stops a live canvas flight in every window open on this document, leaving the canvas where it is.
fun resolveFilmPath(path: String): String A film's path: ~ is the home directory, and a relative path is against the box's working directory.

FlightStop

typealias FlightStop = CanvasFlightStop

One stop of a canvasFlight: a bookmark, and optionally its own leg and hold in seconds.

CanvasFlight

class CanvasFlight internal constructor(val durationSeconds: Double, val path: String?, private val outcome: CompletableFuture<AgentCanvasFlightResponse>)

A canvas flight under way. A live flight is done at once (the windows fly it on their own); a filmed one when its MP4 is written, and result then says how many frames went into it and where each bookmark was reached.

Declaration Summary
val durationSeconds: Double How long the flight lasts, holds included.
val path: String? The MP4 being filmed, or null for a live flight.
val done: Boolean
val result: AgentCanvasFlightResponse? The finished film's report, or null while it is still filming.
val frames: Int? Frames in the finished film, or null while it is still filming.
fun await(timeoutSeconds: Double = 3600.0): AgentCanvasFlightResponse Waits up to timeoutSeconds for the film to finish. Do not call it from box code's main thread.
override fun toString(): String

BoxLayout.kt

"Lay out selection" — arranging boxes on the canvas the way a diagram tool would, using the layout engines in native/field-d2-layout.

A document's boxes and the execution edges between them are a node-link diagram that happens to be editable, so the same engine that draws a Petri net can tidy one. What makes this different from drawing a Petri net is that the positions here are the user's: they were placed by hand, they persist, and they are what the document looks like when it is opened again. So this only ever runs when asked, only over what was asked for, and every move goes through OperationTranscript.recordMove so one undo puts everything back.

Unlike afield.simulation.petri.PetriLayout.TALA there is no fallback: Field has no second box-layout engine to drop to, so an absent helper is reported to the user rather than absorbed.

src/jvmMain/kotlin/afield/ui/layout/BoxLayout.kt — read the source

BoxLayout

object BoxLayout

Arranges a selection of boxes with a d2 layout engine, recording every move for a single undo.

Declaration Summary
fun organize(selection: List<Box>, engine: D2Layout.Engine = D2Layout.Engine.TALA, direction: D2Layout.Direction = D2Layout.Direction.RIGHT, animateMs: Double = 450.0): Int Lays selection out and animates the boxes into place.
fun organizeDocument(engine: D2Layout.Engine = D2Layout.Engine.TALA, direction: D2Layout.Direction = D2Layout.Direction.RIGHT, animateMs: Double = 450.0): Int Lays out every box in the current document, whatever is selected.

D2Layout.kt

Field's client for the native/field-d2-layout helper — d2's layout engines, TALA in particular, over a one-shot subprocess.

The helper is optional and this class is built around that. Field does not build it, no Gradle task references it, and it is absent in a fresh checkout. Every entry point here answers null rather than throwing when the binary is missing, is the wrong protocol, times out, or reports an error, and callers are expected to fall back to whatever they did before rather than surface a failure. See native/field-d2-layout/README.md for the build, the wire format, and why this is a subprocess and not a wasm module hosted in-process like libfaust.

One process per call. That looks profligate and is not: the helper starts in low single-digit milliseconds against layouts that cost hundreds, so spawning is lost in the noise, and a short-lived process means no state to corrupt and nothing to reap if Field exits mid-layout. Repeated identical requests are served from cache instead, which is what actually matters — a Petri animation redraws the same net many times.

src/jvmMain/kotlin/afield/ui/layout/D2Layout.kt — read the source

D2Layout

object D2Layout

Runs d2 layout engines through the optional native/field-d2-layout helper, answering null when it is unavailable.

Declaration Summary
data class Node(val id: String, val w: Double, val h: Double, val parent: String? = null)
data class Edge(val src: String, val dst: String, val id: String? = null)
data class Placed(val id: String, val parent: String = "", val x: Double, val y: Double, val w: Double, val h: Double)
data class Routed(val id: String, val src: String, val dst: String, val route: List<List<Double>> = emptyList())
data class Bounds(val x: Double, val y: Double, val w: Double, val h: Double)
data class Response(val protocol: Int = 0, val ok: Boolean = false, val error: String? = null, val engine: String? = null, val elapsedMs: Double = 0.0, val bounds: Bounds? = null, val nodes: List<Placed> = emptyList(), val edges: List<Routed> = emptyList(), @SerialName("d2Version") val d2Version: String? = null, val engines: List<String> = emptyList())
enum class Engine(val wire: String) d2's three bundled engines. TALA is the one built for diagrams of labelled boxes.
enum class Coordinates(val wire: String) Whether a nested node's position is measured from the graph origin or from its parent.
enum class Direction(val wire: String?) A bias on the whole layout, or NONE to let the engine decide.
fun reset() Forgets the availability probe and every memoised layout.
val version: String?
val available: Boolean True when the helper is present and speaks a protocol this Field understands.
val unavailableHint: String? What to show a user who asked for something this helper does, when it cannot be had.
fun layout(nodes: List<Node>, edges: List<Edge>, engine: Engine = Engine.TALA, coordinates: Coordinates = Coordinates.ABSOLUTE, seeds: List<Long>? = null, direction: Direction = Direction.NONE, timeoutMs: Long = DEFAULT_TIMEOUT_MS): Response? Lays nodes out under edges, or returns null if the helper cannot do it.
fun clearCache() Drops every memoised layout. For a test, or after rebuilding the helper under a live Field.

alignment.kt

Geometric tidying of box frames on the canvas: aligning, snapping and connection-guided routing over a list of Rects, plus the palette actions that apply them to a selection.

Three entry points, each taking rectangles and returning new ones in the same order:

  • alignAndDistribute lines the rectangles up in bands, vertically or horizontally (Orientation), spaced by LayoutParams.
  • snapAndTidyLocal snaps near-equal sizes and edges together, snaps positions, then pushes overlapping rectangles apart.
  • routeAndTidyLocal also takes the connections between rectangles as EdgeIds. It snaps sizes and edges, arranges the rectangles in layers along those edges (RouteParams), then runs the tidy pass and an optional final columnizing pass.

snapAndTidyLocal and routeAndTidyLocal can translate their result back so the group's centroid (CentroidMode) does not move: preserve for the first, preserveCentroid in RouteParams for the second. installAlignment runs on each new root and adds the align, snap_tidy and route actions, which call these with fixed parameters and write the results to the selected boxes' frames; align also restores the centroid.

src/jvmMain/kotlin/afield/ui/layout/alignment.kt — read the source

Top-level functions

Declaration Summary
fun alignAndDistribute(original: List<Rect>, params: LayoutParams = LayoutParams()): List<Rect>
fun snapAndTidyLocal(rects: List<Rect>, sizeSnap: SizeSnapParams = SizeSnapParams(), edgeSnap: EdgeSnapParams = EdgeSnapParams(), tidy: LocalTidyParams = LocalTidyParams(), preserve: Boolean = true, centroidMode: CentroidMode = CentroidMode.AREA_WEIGHTED): List<Rect>
fun routeAndTidyLocal(rects: List<Rect>, edges: List<EdgeId>, route: RouteParams = RouteParams(), sizeSnap: SizeSnapParams = SizeSnapParams(), edgeSnap: EdgeSnapParams = EdgeSnapParams(), tidy: LocalTidyParams = LocalTidyParams()): List<Rect>
fun installAlignment(root: Box)

Orientation

enum class Orientation

The axis boxes are lined up along.

Constants: VERTICAL, HORIZONTAL.

IntraDistribution

enum class IntraDistribution

Constants: EQUAL_GAPS, PACK_MIN_MOVE.

LayoutParams

data class LayoutParams(val intraGap: Double = 0.0, val crossGap: Double = 16.0, val verticalPreferenceRatio: Double = 0.95, val singleBandOverfillTolerance: Double = 0.08, val intraDistribution: IntraDistribution = IntraDistribution.EQUAL_GAPS)

Declaration Summary
val intraGap: Double
val crossGap: Double
val verticalPreferenceRatio: Double
val singleBandOverfillTolerance: Double
val intraDistribution: IntraDistribution

RouteAxis

enum class RouteAxis

Constants: HORIZONTAL, VERTICAL.

RouteParams

data class RouteParams(val layerGap: Double = 240.0, val nodeGap: Double = 36.0, val inertia: Double = 0.15, val maxMove: Double = 10000.0, val origin: Vec2 = vec(0.0, 0.0), val preferNearOrigin: Double = 1.5, val preferInDegree0: Double = 3.0, val preferOutDegree: Double = 0.25, val axis: RouteAxis = RouteAxis.HORIZONTAL, val directionFromPositions: Boolean = true, val crossingIterations: Int = 12, val orthoRelaxIterations: Int = 10, val orthoBaryWeight: Double = 0.9, val dummyW: Double = 12.0, val dummyH: Double = 12.0, val finalColumnize: Boolean = true, val finalColumnizePasses: Int = 1, val preserveCentroid: Boolean = true, val centroidMode: CentroidMode = CentroidMode.AREA_WEIGHTED)

Declaration Summary
val layerGap: Double
val nodeGap: Double
val inertia: Double
val maxMove: Double
val origin: Vec2
val preferNearOrigin: Double
val preferInDegree0: Double
val preferOutDegree: Double
val axis: RouteAxis
val directionFromPositions: Boolean
val crossingIterations: Int
val orthoRelaxIterations: Int
val orthoBaryWeight: Double
val dummyW: Double
val dummyH: Double
val finalColumnize: Boolean
val finalColumnizePasses: Int
val preserveCentroid: Boolean
val centroidMode: CentroidMode

GuideMode1D

enum class GuideMode1D

Constants: MEAN, MEDIAN.

EdgeAnchor

enum class EdgeAnchor

Constants: LEFT, RIGHT, TOP, BOTTOM.

SizeSnapParams

data class SizeSnapParams(val widthSnapTol: Double = 14.0, val heightSnapTol: Double = 14.0, val mode: GuideMode1D = GuideMode1D.MEDIAN, val minW: Double = 8.0, val minH: Double = 8.0, val maxDeltaPerRect: Double = 1000.0, val preserveCenter: Boolean = false, val onlyIfClusterAtLeast: Int = 2)

Declaration Summary
val widthSnapTol: Double
val heightSnapTol: Double
val mode: GuideMode1D
val minW: Double
val minH: Double
val maxDeltaPerRect: Double
val preserveCenter: Boolean
val onlyIfClusterAtLeast: Int

EdgeSnapParams

data class EdgeSnapParams(val tol: Double = 14.0, val anchors: Set<EdgeAnchor> = setOf(EdgeAnchor.RIGHT, EdgeAnchor.BOTTOM), val maxDeltaPerRect: Double = 1000.0, val onlyIfClusterAtLeast: Int = 2)

Declaration Summary
val tol: Double
val anchors: Set<EdgeAnchor>
val maxDeltaPerRect: Double
val onlyIfClusterAtLeast: Int

LocalTidyParams

data class LocalTidyParams(val posSnapTol: Double = 10.0, val minGap: Double = 24.0, val overlapPasses: Int = 4)

Declaration Summary
val posSnapTol: Double
val minGap: Double
val overlapPasses: Int

EdgeId

data class EdgeId(val fromId: String, val toId: String, val weight: Double = 1.0)

CentroidMode

enum class CentroidMode

Constants: UNWEIGHTED, AREA_WEIGHTED.

DarkThemeColour.kt

Colour correction for marks written from code, so that they show as asked under the drawing layer's dark theme rather than as their inverted, hue-rotated selves.

src/jvmMain/kotlin/afield/ui/notation/DarkThemeColour.kt — read the source

DarkThemeColour

object DarkThemeColour

Colours as the drawing layer shows them. Excalidraw's dark theme draws the scene through the CSS filter invert(93%) hue-rotate(180deg) (theme="dark" in tools/excalidraw-bundle/entry.jsx), so a colour stored on an element is not the colour on screen. Per channel the inversion is 0.93 - 0.86 c; the hue rotation is the CSS filter matrix H for 180°.

Declaration Summary
const val DEFAULT_INK
fun shown(storedHex: String): String What the dark theme shows for a stored #rrggbb.
fun compensate(requestedHex: String): String The #rrggbb to store so the dark theme shows requestedHex; anything that is not #rrggbb passes through.

Notation.kt

The box-facing API for the drawing layer: notation reads the document's Excalidraw scene as Marks, writes marks owned by the box through a notation { } block (NotationBuilder), changes existing ones with Notation.edit, and runs callbacks when a person draws.

Marks a box writes are replaced when that box re-runs, so a re-run redraws rather than accumulates. Reactions such as notation.onStroke { } fire only for changes made by hand in a client, not for marks written from code. Coverage is in NotationTest and NotationIntersectionTest; the full API is in dev_docs/NOTATION.md.

src/jvmMain/kotlin/afield/ui/notation/Notation.kt — read the source

Used in: Drawn Strokes Become Paint, Drawing and Notation, A Drawn Score, Heard

Top-level functions

Declaration Summary
fun NotationBuilder.line(fline: FLine, name: String? = null, stroke: String = this.stroke): MarkRef NotationBuilder.line through the points of an FLine (curves are sampled).
fun NotationBuilder.stroke(fline: FLine, name: String? = null, stroke: String = this.stroke, pressure: ((Double) -> Double)? = null): MarkRef NotationBuilder.stroke along an FLine (curves are sampled).

Top-level values

Declaration Summary
val Box.notation: Notation The document's drawing, as seen from this box — read it, write to it, and react to it.

MarkHandle

interface MarkHandle

Something a mark can be referred to by: a Mark read from the scene, or a MarkRef just built.

Declaration Summary
val id: String

MarkRef

data class MarkRef(override val id: String) : MarkHandle

A mark created in a notation { } block, before it has been read back from the scene.

Mark

class Mark internal constructor(val json: JsonObject, private val view: Notation) : MarkHandle

One element of the drawing — a stroke, a shape, a text, an arrow — read from the scene.

Declaration Summary
val json: JsonObject The element exactly as Excalidraw stores it.
override val id: String
val kind: MarkType
val bounds: Rect World-space bounds (unrotated), in the same units as a box's frame.
val center: Vec2
val angle: Double
val points: List<Vec2> World-space points of a stroke, line or arrow; empty for other kinds.
val pressures: List<Double> The pen pressure Excalidraw recorded at each of points, 0..1, for a stroke drawn with a pressure-sensitive pen (or written from code with pressure =). Empty when the stroke has none: a mouse stroke, whose width Excalidraw simulates from speed instead.
fun pressuresAlong(n: Int): List<Double> pressures at n stations evenly spaced by distance along the stroke, first point to last. A pen records points closer together where it moves slowly, so the raw list is not evenly spaced; this one is, which is what a PaintCanvas brush's swell and a pencil's pressure profile expect. Empty when the stroke recorded no pressure.
val start: Vec2?
val end: Vec2?
val stroke: String The colour code asked for (kept in customData); a hand-drawn mark's stored colour.
val fill: String
val strokeWidth: Double
val opacity: Double
val text: String? The text of a text mark.
val boundText: Mark? The text bound inside a shape or on an arrow, if there is one.
val label: String? A name given from code (name =), else the mark's own text, else the text bound to it, else — for a frame — the title Excalidraw keeps in the frame's own name field, which is where a frame named by hand has its name.
val name: String? The name given from code with name =, or through Notation.edit.
val owner: Box? The box whose notation { } block wrote this mark; null for a mark drawn by hand.
val ownerId: String?
val handDrawn: Boolean
val box: Box? The box under this mark's centre, if any (the smallest, when boxes overlap).
val fromBox: Box? For an arrow or line: the box its first point is on.
val toBox: Box? For an arrow or line: the box its last point is on.
val anchoredTo: Box? The box this mark follows when boxes move, if it is anchored.
val frame: Mark? The Excalidraw frame this mark is in, if any.
val groups: List<String> The Excalidraw groups this mark is in, innermost first (Excalidraw's own order).
fun anchorTo(box: Box) Makes this mark follow box when it moves; the same as notation.anchor(this, box).
fun contains(p: Vec2): Boolean
fun toFLine(): FLine The mark's geometry in world coordinates, rotation included: a stroke's polyline, or a shape's outline.
fun yAt(x: Double): List<Double> Where this mark crosses the vertical line at world x: its y values there (a shape gives its edges).
fun xAt(y: Double): List<Double> Where this mark crosses the horizontal line at world y: its x values there.
val length: Double The length of the mark's path (a shape's perimeter), in world units.
override fun toString(): String
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

NotationEdit

class NotationEdit internal constructor(val added: List<Mark>, val updated: List<Mark>, val removed: List<Mark>, val notation: Notation)

What changed in the drawing, handed to Notation.onChange.

Declaration Summary
val added: List<Mark>
val updated: List<Mark>
val removed: List<Mark>
val notation: Notation The drawing as it now stands.

Notation

class Notation internal constructor(private val box: Box)

The drawing, as seen from one box. notation in box code gives you one; it reads the scene when you ask it something, so a Notation kept in a variable still answers with the current drawing.

Declaration Summary
val marks: List<Mark> Every mark in the drawing, back to front.
val strokes: List<Mark>
val arrows: List<Mark>
val lines: List<Mark>
val texts: List<Mark>
val shapes: List<Mark>
val frames: List<Mark>
val mine: List<Mark> The marks this box wrote, from every notation { } block.
val selected: List<Mark> What is selected in the drawing layer right now, in any window showing this document.
operator fun get(id: String): Mark?
fun named(name: String): Mark? The first mark labelled name — a code-given name, its own text, or the text bound to it.
fun allNamed(name: String): List<Mark>
fun over(box: Box): List<Mark> Marks whose centre lies on box.
fun inside(rect: Rect): List<Mark> Marks entirely inside rect.
fun inside(frame: MarkHandle): List<Mark> The marks in an Excalidraw frame.
fun grouped(mark: MarkHandle): List<Mark> The marks that move with mark when it is selected and dragged: everything sharing its outermost group (itself included), or just mark when it is in no group.
fun near(p: Vec2, radius: Double): List<Mark> Marks whose bounds come within radius of p, nearest first.
fun arrowsFrom(box: Box): List<Mark> Arrows (and lines) whose first point is on box.
fun arrowsTo(box: Box): List<Mark> Arrows (and lines) whose last point is on box.
fun vertical(x: Double): List<Hit> Cut the whole drawing with the vertical line at world x; hit values are world y.
fun horizontal(y: Double): List<Hit> Cut the whole drawing with the horizontal line at world y; hit values are world x.
fun intersect(from: Vec2, to: Vec2): List<Hit> Every place the segment from–to crosses a mark, nearest from first; hit values are 0..1 along the segment.
fun intersect(probe: FLine): List<Hit> Every place an FLine (a probe of any shape) crosses a mark, in order along the FLine.
fun space(region: Rect, x: ClosedFloatingPointRange<Double>, y: ClosedFloatingPointRange<Double>): NotationSpace A region of the drawing read with its own axes — see NotationSpace. region is the rectangle; x and y are what its left→right and top→bottom mean.
fun space(region: Box, x: ClosedFloatingPointRange<Double>, y: ClosedFloatingPointRange<Double>): NotationSpace A region that is a box's frame: move the box and the space moves with it.
fun space(name: String, x: ClosedFloatingPointRange<Double>, y: ClosedFloatingPointRange<Double>): NotationSpace A region that is the frame (or any mark) labelled name, wherever it is now.
fun space(mark: MarkHandle, x: ClosedFloatingPointRange<Double>, y: ClosedFloatingPointRange<Double>): NotationSpace A region that is mark — a frame or shape drawn by hand or returned by notation { } — wherever it is now.
operator fun invoke(key: String = "default", build: NotationBuilder.() -> Unit): List<MarkRef> Draws from code. The marks made here belong to this box under key: running the block again replaces them (updating in place, so the drawing does not flicker), and marks drawn by hand are never touched.
fun clear(key: String? = null) Removes the marks this box drew — under key, or under every key when key is null.
fun edit(mark: MarkHandle, block: MarkEditor.() -> Unit): Mark? Changes any mark — including one drawn by hand.
fun delete(mark: MarkHandle)
fun anchor(mark: MarkHandle, to: Box) Makes mark follow box when the box moves.
fun unanchor(mark: MarkHandle)
fun onChange(key: String = "change", handler: (NotationEdit) -> Unit) Calls handler whenever someone changes the drawing by hand. Running the box again replaces the handler registered under the same key; stopping the box removes it. Changes this code makes itself do not call it back.
fun onStroke(key: String = "stroke", handler: (Mark) -> Unit) Calls handler for each stroke drawn by hand, once it is finished.
fun onArrow(key: String = "arrow", handler: (Mark) -> Unit) Calls handler for each arrow drawn by hand, and again when its ends are moved.
fun onText(key: String = "text", handler: (Mark) -> Unit) Calls handler for each text typed by hand, and again when it is edited.

MarkEditor

class MarkEditor internal constructor(private val original: JsonObject)

Sets the fields of one mark; see Notation.edit. Unset fields keep their values.

Declaration Summary
var x: Double
var y: Double
var width: Double
var height: Double
var angle: Double
var stroke: String The colour asked for; stored compensated for the dark theme (DarkThemeColour).
var fill: String
var strokeWidth: Double
var opacity: Double
var roughness: Double How hand-drawn the line looks: 0 architect, 1 artist (the default), 2 cartoonist.
var strokeStyle: String "solid", "dashed" or "dotted".
var dashed: Boolean
var fillStyle: String How a fill is painted: "solid", "hachure", "cross-hatch" or "zigzag".
var text: String?
var name: String?
fun moveBy(dx: Double, dy: Double)

NotationBuilder

class NotationBuilder internal constructor(private val owner: Box, private val key: String, private val view: Notation)

The drawing vocabulary of a notation { } block. Every call makes one mark owned by the box and returns a MarkRef to it. The vars set defaults for the calls that follow them.

Declaration Summary
var stroke: String
var fill: String
var strokeWidth: Double
var roughness: Double
var opacity: Double
var dashed: Boolean
var fontSize: Double
var anchorTo: Box? When set, the marks made after it follow this box when it moves.
var fillStyle: String? How a non-transparent fill is painted; null means solid. See MarkEditor.fillStyle.
fun group(contents: NotationBuilder.() -> Unit): String Groups the marks made in contents the way Excalidraw's Ctrl+G does: selecting one selects them all, and they drag together. Groups nest. Returns the group's id.
fun rect(x: Number, y: Number, w: Number, h: Number, name: String? = null, label: String? = null, stroke: String = this.stroke, fill: String = this.fill, fillStyle: String? = this.fillStyle, opacity: Number = this.opacity): MarkRef
fun ellipse(x: Number, y: Number, w: Number, h: Number, name: String? = null, label: String? = null, stroke: String = this.stroke, fill: String = this.fill, fillStyle: String? = this.fillStyle, opacity: Number = this.opacity): MarkRef
fun diamond(x: Number, y: Number, w: Number, h: Number, name: String? = null, label: String? = null, stroke: String = this.stroke, fill: String = this.fill, fillStyle: String? = this.fillStyle, opacity: Number = this.opacity): MarkRef
fun text(text: String, x: Number, y: Number, size: Number = fontSize, name: String? = null, color: String = stroke): MarkRef
fun line(points: List<Vec2>, name: String? = null, stroke: String = this.stroke): MarkRef A straight or bent line through points.
fun stroke(points: List<Vec2>, name: String? = null, stroke: String = this.stroke, pressure: ((Double) -> Double)? = null): MarkRef A freehand stroke — the same kind of mark as one drawn with the pen tool. Give pressure (a function of the fraction of the way along the stroke, 0..1, returning 0..1) to record pen pressure the way a tablet does; without it the stroke is like a mouse stroke, and Mark.pressures reads back empty.
fun arrow(from: Vec2, to: Vec2, label: String? = null, name: String? = null): MarkRef An arrow between two points.
fun arrow(from: Box, to: Box, label: String? = null, name: String? = null): MarkRef An arrow from one box to another. Its ends sit on the boxes' edges and follow the boxes as they move.
fun arrow(from: Box, to: Vec2, label: String? = null, name: String? = null): MarkRef
fun arrow(from: Vec2, to: Box, label: String? = null, name: String? = null): MarkRef
fun arrow(from: MarkHandle, to: MarkHandle, label: String? = null, name: String? = null): MarkRef An arrow bound, the way Excalidraw binds them, to two marks: dragging either mark by hand keeps the arrow attached.
fun frame(name: String, x: Number, y: Number, w: Number, h: Number, contents: (NotationBuilder.() -> Unit)? = null): MarkRef An Excalidraw frame: a named region that groups the marks drawn inside it. Marks made in contents belong to the frame — Excalidraw clips them to it and drags them with it, and Notation.inside finds them.

NotationGeometry.kt

Cutting the drawing with a line: the geometry behind reading notation by intersection — a vertical line is an instant, a horizontal one a pitch (or whatever a NotationSpace says the axes mean).

Every mark is flattened once per scene revision into world-space polylines with its rotation applied (MarkGeometry); probes then only walk segments. The results are plain values, safe to keep and to read from any thread.

src/jvmMain/kotlin/afield/ui/notation/NotationGeometry.kt — read the source

ProbeAxis

enum class ProbeAxis

Which way a probe line runs.

Constants: VERTICAL, HORIZONTAL.

Hit

class Hit internal constructor(val mark: Mark, val point: Vec2, val value: Double, val span: ClosedFloatingPointRange<Double>?, val along: Double, val slope: Double, val pressure: Double?)

Where a probe line meets a mark.

Declaration Summary
val mark: Mark
val point: Vec2 The crossing in world coordinates.
val value: Double Position along the probe, in the probe's units (world y for a vertical world probe; pitch for a score's).
val span: ClosedFloatingPointRange<Double>? For closed marks: the stretch of the probe inside the mark, in the same units as value.
val along: Double How far along the mark's own path the crossing is, 0 at its first point and 1 at its last.
val slope: Double The mark's rate of change across the probe there, in the probe's units: d(value)/d(position).
val pressure: Double? Pen pressure at the crossing, for a stroke that recorded it (0..1); otherwise null.
val isSpan: Boolean
override fun toString(): String

NotationSpace

class NotationSpace internal constructor(private val notation: Notation, private val region: () -> Rect?, val x: ClosedFloatingPointRange<Double>, val y: ClosedFloatingPointRange<Double>, private val defining: () -> String? = { null })

A region of the drawing with its own axes — a score, a graph, a map. Horizontal position in the region becomes the x range and vertical position the y range, so notation.space("score", x = 0.0..8.0, y = 84.0..48.0) reads the frame named "score" as eight beats across and a MIDI pitch range down (high at the top, because Field's y grows downward).

Declaration Summary
val x: ClosedFloatingPointRange<Double>
val y: ClosedFloatingPointRange<Double>
val bounds: Rect? The region in world coordinates, as it is now (null if it has gone).
fun toSpace(world: Vec2): Vec2
fun toWorld(sx: Double, sy: Double): Vec2
val marks: List<Mark> Marks that overlap the region, excluding the frame (or shape) that defines it — found by its id, so a note that happens to share the frame's bounds still counts. A stroke that runs out of the region still counts; what lies outside is clipped away by the cuts and events.
fun vertical(at: Double): List<Hit> Cut the region at at across: what is sounding at that instant, and at what y values.
fun horizontal(at: Double): List<Hit> Cut the region at at along: where marks pass that y value, as x values.
val events: List<NotationEvent> Every mark in the region as an event: where it starts and ends along x, and its y value at its start and end — a note's onset, offset and pitch; a gesture's first and last pitch. Sorted by start.
fun between(from: Double, until: Double): List<NotationEvent> Events starting in [from, until) — what a playhead moving from from to until has just reached.
fun sounding(at: Double): List<NotationEvent> Events sounding at at: started at or before it and not yet ended.

NotationEvent

class NotationEvent internal constructor(val mark: Mark, val start: Double, val end: Double, val value: Double, val endValue: Double, val range: ClosedFloatingPointRange<Double>)

A mark read as an event in a NotationSpace: it starts and ends along the space's x axis, with value at its start and endValue at its end on the y axis, and covers range on y.

Declaration Summary
val mark: Mark
val start: Double
val end: Double
val value: Double
val endValue: Double
val range: ClosedFloatingPointRange<Double>
val duration: Double
override fun toString(): String

NotationScene.kt

The drawing layer's scene as immutable data: NotationScene parses and re-serializes the root box's drawing property, NotationChange describes what differs between two scenes, and MarkType names the Excalidraw element kinds.

Merging two copies of a scene keeps, for each element id, the copy Excalidraw's own version rule prefers, so the server can reconcile a client's drawing with marks written from code.

src/jvmMain/kotlin/afield/ui/notation/NotationScene.kt — read the source

NotationScene

class NotationScene private constructor(private val envelope: JsonObject, val elements: List<JsonObject>)

The drawing layer's scene as data: the root box's drawing property, a complete .excalidraw document, parsed losslessly.

Declaration Summary
val elements: List<JsonObject> Elements in z order, deleted ones included: they are the tombstones that carry a deletion.
operator fun get(id: String): JsonObject?
val live: List<JsonObject> Live (non-deleted) elements in z order.
fun serialize(): String
fun withElement(element: JsonObject): NotationScene Replaces (by id) or appends element, bumping its version past whatever this scene holds.
fun withElements(changed: List<JsonObject>): NotationScene

NotationChange

class NotationChange internal constructor(val added: List<JsonObject>, val updated: List<JsonObject>, val removed: List<JsonObject>)

What changed between two scenes, for reactions. Only live elements are reported as added/updated.

Declaration Summary
val added: List<JsonObject>
val updated: List<JsonObject>
val removed: List<JsonObject>
val isEmpty: Boolean

MarkType

enum class MarkType

The kinds of mark Excalidraw draws, as Field names them.

Constants: RECTANGLE, ELLIPSE, DIAMOND, LINE, ARROW, STROKE, TEXT, FRAME, IMAGE, OTHER.

Declaration Summary
val isLinear: Boolean

NotationServer.kt

The server end of the drawing layer: installNotationHandlers registers the websocket handlers that receive each client's scene and selection, merge the scene into the document's copy, and pass what a person changed on to the box reactions and to arrow anchoring.

Anchoring ties a mark, or each end of an arrow, to the box under it, so the drawing follows when boxes move. The drawing layer's Anchor Drawing to Boxes command applies it to the selection.

src/jvmMain/kotlin/afield/ui/notation/NotationServer.kt — read the source

Top-level functions

Declaration Summary
fun installNotationHandlers() The server side of the drawing layer: clients send their scene here (not as a raw set-string), and it is merged into the authoritative one by element version, so a mark written by code and a mark drawn by hand, arriving together, both survive. What a person changed then goes to the reactions (dispatchNotationChange) and to arrow auto-anchoring.

NotationSceneUpdate

data class NotationSceneUpdate(val scene: String)

A client's drawing, as it now stands in that window.

NotationSelection

data class NotationSelection(val ids: List<String>)

What a client has selected in the drawing layer.

StrokeLayerFrames.kt

What a window is sent to draw the stroke layer: every living stroke's lines as compact JSON, and the point index the strokes use to find each other.

StrokeSketch calls both. StrokeLayerFrames.indexedPositions runs once per stroke per physics step, over the lines that stroke has just drawn, and the flat x, y array it returns is what Stroke.closestOutput searches on the step after. StrokeLayerFrames.encode runs once per published frame, and its JSON reaches every window showing the document as stroke-layer-frame; strokeLayer.kt in the frontend draws it.

Only what a canvas needs crosses the wire. Coordinates are world units rounded to a tenth, colours arrive as CSS rather than as vectors, and a per-node colour or dot radius is sent only when some node sets one. Overshoot ticks (FLine.overshoot) are worked out here and sent as finished segments. StrokeVerbsTest asserts the paths, colours, dots and ticks of a frame, and that an ignored line stays out of the index.

src/jvmMain/kotlin/afield/ui/notation/StrokeLayerFrames.kt — read the source

StrokeLayerFrames

object StrokeLayerFrames

One frame of the stroke layer, as the JSON the page draws (strokeLayer.kt in the frontend).

Declaration Summary
fun encode(trail: Double, strokes: List<Pair<List<FLine>, Double>>): String The frame for trail and every stroke's lines with its default width.
fun indexedPositions(lines: List<FLine>): FloatArray The positions Stroke.closestOutput searches: every node end point of lines not marked FLine.ignore, as x, y pairs.
fun css(c: Vec4?): String c as a CSS colour; white when null (an FLine's default).

StrokeSketch.kt

Living strokes: each stroke drawn by hand on the drawing layer becomes an animated, sounding object running code of its own, after the madp line demo.

A box takes the pen with eachStroke. Each stroke then drawn with the drawing layer's pencil is taken off the scene and becomes a Stroke: physics points that start where the hand went, the block of the box that held the pen when it was drawn (later edits do not reach it), a draw { t -> } called sixty times a second, and an optional pattern playing through dough. What the strokes draw is shown on the stroke layer, a canvas in world coordinates between Field's background and its boxes, which fades towards transparent for trails.

One StrokeSketch per document keeps the strokes, the pen and the clock. Its frames go to each window showing the document as stroke-layer-frame, one at a time: the next is sent once the window answers stroke-layer-ack, so a slow window is never flooded.

src/jvmMain/kotlin/afield/ui/notation/StrokeSketch.kt — read the source

Top-level functions

Declaration Summary
fun Box.eachStroke(trail: Double = 0.0, lineWidth: Double = 2.0, keepMarks: Boolean = false, body: Stroke.() -> Unit): StrokePen Take the pen: every stroke drawn by hand from now on (Alt+D, the pencil) becomes a living Stroke running body.
fun Box.clearStrokes() Remove every living stroke in this document, and silence them. The pen stays where it is.
fun installStrokeLayerHandlers() The server end of the stroke layer: a window answers each stroke-layer-frame with stroke-layer-ack once it has drawn it, and only then is it sent the next.

Top-level values

Declaration Summary
val Box.livingStrokes: List<Stroke> The living strokes of this document, oldest first.

StrokePoint

class StrokePoint(var x: Double, var y: Double)

One physics point of a living stroke. Set ax and ay in draw { }; the sketch integrates them.

Declaration Summary
var vx
var vy
var ax
var ay
val at: Vec2 Where the point is now.
override fun toString()

StrokeHit

data class StrokeHit(val x: Double, val y: Double, val dist: Double, val strokeIndex: Int, val pointIndex: Int)

A point another stroke has, found by Stroke.closestPoint or Stroke.closestOutput.

StrokePen

class StrokePen internal constructor(val box: Box, val trail: Double, val lineWidth: Double, internal val body: Stroke.() -> Unit)

What eachStroke made: the block a box gives to every stroke drawn while it holds the pen.

Declaration Summary
val box: Box
val trail: Double How much of the last frame each frame keeps, 0..1: 0 clears, 0.9 leaves long trails.
val lineWidth: Double Width of a line that does not set its own thickness.
val hasPen: Boolean Whether strokes drawn now come to this pen.
override fun toString()

Stroke

class Stroke internal constructor(internal val sketch: StrokeSketch, val pen: StrokePen, val index: Int, val points: List<StrokePoint>, internal val born: Double) : StrokeVerbs()

A stroke that has come alive: its points, its place in the drawing (index), and the code it was drawn with. The receiver of the eachStroke block, which runs once when the stroke is drawn (the demo's onStart) and sets up the rest with draw (its manipulatePoints) and sound.

Declaration Summary
val pen: StrokePen
val index: Int 0 for the first stroke on the page, then 1, 2, ...
val points: List<StrokePoint> The stroke's physics points, where the hand went, about 2 units apart.
val box: Box The box that held the pen when this stroke was drawn.
var t: Double Seconds since this stroke was drawn.
val curve: List<Vec2> The points' current positions, as a curve.
fun draw(block: (t: Double) -> List<FLine>) What this stroke draws, called 60 times a second with t, seconds since it was drawn. Return the lines to show; set ax/ay on points to push them, and the sketch moves them after the call (damping 0.98 a step). Without a draw, the stroke is shown as it was drawn.
fun sound(pattern: PatPattern<String>) Play pattern while this stroke lives, on the sketch's clock (0.5 cycles a second, shared by every stroke, so patterns stay in step). The pattern language's own atoms and params, with the Strudel names s, lpf and signal: pat("c4 e4 g4") with s("sawtooth") with gain(signal { level }). Note names are Tidal's, so c5 is middle C. Gains are Strudel's: the pattern plays at a quarter of dough's level, as quiet as the same gain is in the demo.
val cycle: Double The sketch's sound clock, in cycles: cycle % 1 is how far through the bar it is now.
fun closestPoint(x: Double, y: Double): StrokeHit? The nearest physics point of any other stroke to (x, y), or null when this is the only one.
fun closestOutput(x: Double, y: Double): StrokeHit? The nearest point of what any other stroke drew last frame to (x, y): every node of its lines, except lines marked FLine.ignore. Last frame's, because this frame's is still being drawn.
fun rms(): Double How loud everything the strokes play is right now, measured from dough's voices: about 0.7 for a sine at gain 1.
fun s(name: String): PatParam Strudel's .sound(name) / s(name): the dough sound a note plays on (sawtooth, triangle, square, sine).
fun lpf(values: PatPattern<Double>): PatParam Strudel's .lpf(hz): dough's low-pass cutoff.
fun lpf(hz: Number): PatParam lpf at a fixed cutoff.
fun signal(read: () -> Double): PatPattern<Double> Strudel's signal(() => …): a continuous pattern whose value is read when each event is scheduled.
override fun toString()

StrokeSketch

class StrokeSketch internal constructor(val info: SessionInformation)

The strokes of one document, the pen that new strokes go to, and the clock that steps them.

Declaration Summary
val strokes The strokes, oldest first.
var pen: StrokePen? Where new strokes go, or null when no box holds the pen.
var trail: Double The trail of the most recent pen: how much of each frame the next keeps.
val cycle: Double The sound clock, in cycles since the first stroke played; 0 before any did.
override fun toString()

StrokeVerbs.kt

The drawing vocabulary of a living stroke: Perlin noise, curve maths, and the verbs that turn a list of points into an FLine.

StrokeVerbs carries the verbs, and Stroke inherits it, so eachStroke { } code calls them directly. Some are short names over the shared geometry: smooth is catmullRomCentripetal and atomize is FLine.byAtomizing, while wiggle and transformTo have a curve form of their own and a line form that calls FLine.byWiggling and FLine.byTransformingTo. The noise, the resampling and the rest of the curve arithmetic are this file's own.

The three FLine properties here are read by the stroke layer rather than by any verb. FLine.overshoot runs each open subpath of a line past its ends, a node's overshoot overrides that for the subpath it ends, and FLine.ignore keeps a line out of the point index Stroke.closestOutput searches, so a guide or a stitch does not attract the strokes around it.

StrokeVerbsTest pins the resampling, the curve maths and the noise.

src/jvmMain/kotlin/afield/ui/notation/StrokeVerbs.kt — read the source

Top-level values

Declaration Summary
var FLine.overshoot: Double? How far past each end the stroke layer draws every subpath of this line, along its tangent, in the line's own units: the sketchy overrun of a pen that does not stop exactly where it means to. null (the default) draws none. Only the stroke layer (eachStroke) draws it.
var FLine.Node.overshoot: Double? Overshoot for the subpath this node ends, overriding the line's FLine.overshoot at both of that subpath's ends. A line of separate segments (StrokeVerbs.atomize) can so overrun each segment by its own amount.
var FLine.ignore: Boolean Leave this line out of what other strokes find with Stroke.closestOutput: guides, stitches and other decoration a neighbouring stroke should not be drawn towards. false by default.

StrokeVerbs

open class StrokeVerbs

The vocabulary a living stroke is written in (eachStroke): the madp line demo's noise, curve maths and drawing verbs, under the demo's names.

Declaration Summary
fun perlin2D(x: Double, y: Double): Double Classic 2D Perlin noise, about -1..1, with the madp demos' permutation table.
fun clamp01(v: Double): Double v clamped to 0..1.
fun triWave(x: Double): Double A triangle wave of period 1 in -1..1: -1 at whole numbers, 1 half way between.
fun tri01(x: Double): Double triWave in 0..1.
fun heldNoise(seed: Double, t: Double, rate: Double): Double Noise that holds still and then jumps, rate times a second: broken, stepped motion.
fun driftNoise(seed: Double, t: Double, rate: Double): Double Noise that drifts smoothly through time at rate: breathing motion.
fun length(curve: List<Vec2>): Double The length of curve, along it.
fun resample(curve: List<Vec2>, n: Int): List<Vec2> curve resampled to n points evenly spaced along it, ends kept. A curve too short to resample comes back as it is.
fun resample(points: List<StrokePoint>, n: Int): List<Vec2> A stroke's points, at their current positions, resampled to n points.
fun warp(curve: List<Vec2>, fn: (Vec2) -> Vec2): List<Vec2> Every point of curve moved by fn.
fun wiggle(curve: List<Vec2>, amplitude: Double = 10.0, frequency: Double = 0.05, phase: Double = 0.0): List<Vec2> curve pushed about by Perlin noise: amplitude units, frequency cycles per unit, phase slides the field.
fun wiggle(line: FLine, amplitude: Double = 10.0, frequency: Double = 0.05, phase: Double = 0.0): FLine Every node of line pushed about by Perlin noise, as wiggle does to a curve.
fun transformTo(curve: List<Vec2>, start: Vec2, end: Vec2): List<Vec2> curve moved, rotated and scaled as one shape so its first point lands on start and its last on end.
fun transformTo(line: FLine, start: Vec2, end: Vec2): FLine line moved, rotated and scaled so its first node lands on start and its last on end.
fun add(a: List<Vec2>, b: List<Vec2>): List<Vec2> a displaced by b's shape: both are resampled to their average point count, and each point of a moves by how far the matching point of b is from b's centroid.
fun subtract(a: List<Vec2>, b: List<Vec2>): List<Vec2> a displaced against b's shape: add with b's displacements reversed.
fun project(a: List<Vec2>, b: List<Vec2>): List<Vec2> Each point of a, as a displacement from a's centroid, projected onto the direction of the matching point of b from b's centroid: a squeezed into b's directions.
fun scale(curve: List<Vec2>, factor: Double): List<Vec2> curve scaled by factor about its centroid.
fun scale(curve: List<Vec2>, by: List<Vec2>): List<Vec2> curve scaled point by point by how far by's matching point is from by's centroid, relative to its mean radius.
fun normalAt(curve: List<Vec2>, i: Int): Vec2 The unit normal of curve at point i, from its neighbours: the tangent turned a quarter turn.
fun tangentAt(curve: List<Vec2>, i: Int): Vec2 The unit tangent of curve at point i, from its neighbours.
fun envelope(i: Int, n: Int): Double 0 at both ends of a curve of n points, 1 in the middle: 1 - \|2i/(n-1) - 1\|.
fun polyline(curve: List<Vec2>): FLine curve as straight segments.
fun smooth(curve: List<Vec2>): FLine curve as a smooth line through every point: a centripetal Catmull-Rom spline (catmullRomCentripetal).
fun arcs(curve: List<Vec2>, segmentsPerArc: Int = 10): FLine curve as a chain of circular arcs: from its third point on, each point is reached along the circle through it and the two points before it (a straight line where they are in a line).
fun dots(curve: List<Vec2>, radius: Double = 2.0): FLine A dot of radius at every point of curve: an unstroked, pointed line.
fun dots(line: FLine, radius: Double = 2.0): FLine A dot of radius at every node of line, each keeping its node's colour.
fun FLine.dot(x: Number, y: Number, radius: Double, color: Vec4? = null): FLine Adds a dot of radius at (x, y) to this line, coloured color (the line's colour when null). Make the line pointed and unstroked to draw dots alone.
fun FLine.segment(x0: Number, y0: Number, x1: Number, y1: Number, color: Vec4? = null): FLine Adds the separate segment (x0, y0) to (x1, y1) to this line, coloured color when given.
fun atomize(curve: List<Vec2>): FLine curve as separate segments, one subpath per step between neighbouring points.
fun atomize(line: FLine): FLine line as separate segments (byAtomizing).

CharGrid.kt

A grid of glyphs that box code writes into, cell by cell, shown in an output panel: generative text art in the spirit of 10 PRINT, ported from the madp charGrid demo.

The program writes glyph indices with SET, and a charset string (glyphs) turns each index into a character; a cell can also carry a CSS colour and raw CSS such as transform:rotate(45deg). charGrid runs its block once per tick with the previous tick readable through PREV; charStream runs one long suspending program in which every SET waits for the next tick. A glyph can be a dough sound (Box.sound), so every SET of it plays. The panel pulls only the cells that changed since it last asked (CharGridBuffer.update), so a 200 × 100 grid at 24 fps sends a frame's changes, not the whole grid.

src/jvmMain/kotlin/afield/ui/output/CharGrid.kt — read the source

Top-level functions

Declaration Summary
fun Box.charGrid(cols: Int = 40, rows: Int = 25, glyphs: String = CHARGRID_GLYPHS, fps: Double = 24.0, stretch: Boolean = true, sounds: List<String> = emptyList(), sound: (GlyphSound.() -> String?)? = null, channel: String = "chargrid", width: Int? = null, body: CharGridFrame.() -> Unit): Frames A char grid whose body runs once per tick, the demo's frame mode.
fun Box.charStream(cols: Int = 40, rows: Int = 25, glyphs: String = CHARGRID_GLYPHS, fps: Double = 24.0, stretch: Boolean = true, sounds: List<String> = emptyList(), sound: (GlyphSound.() -> String?)? = null, channel: String = "chargrid", width: Int? = null, program: suspend CharGridStream.() -> Unit): Frames A char grid run by one long program, the demo's stream mode: every SET writes and then waits for the next tick, and waitFrame() waits a tick, so while (true) SET(…) draws one cell a tick.

Top-level values

Declaration Summary
const val CHARGRID_GLYPHS The charset the demo opens with.
val CHARGRID_ASCII: String Printable ASCII, space to tilde: the charset for print and setText programs, so glyphIndex(ch) finds any key.

CharGridCursor

data class CharGridCursor(val x: Int, val y: Int)

Where the SET(g) cursor is: the column and row the next cursor SET writes.

CharGridBuffer

class CharGridBuffer(cols: Int, rows: Int, glyphs: String)

The cells of a char grid and the encoder that sends their changes to the panel.

Declaration Summary
val cols: Int Columns, clamped to 1..MAX_COLS.
val rows: Int Rows, clamped to 1..MAX_ROWS.
val glyphs: String The charset, as given (a space if empty).
val glyphList: List<String> The charset split into characters (code points), the panel's lookup table.
var epoch: Int Bumped when the string tables are rebuilt; a panel on another epoch gets a full frame.
var cursorX: Int The cursor column.
var cursorY: Int The cursor row.
var frame: Int The tick number shown in the panel's status line.
fun set(x: Int, y: Int, g: Int, color: String? = null, style: String? = null): Boolean Write glyph g at (x, y); color null keeps the cell's colour, style null clears its CSS (the demo's rules). Returns false, writing nothing, outside the grid. Moves the cursor there.
fun setAtCursor(g: Int, color: String? = null, style: String? = null): CharGridCursor Write glyph g at the cursor and advance it, wrapping at the right and scrolling at the bottom. Returns where it wrote.
fun newLine() Move the cursor to the start of the next line, scrolling the grid up a row at the bottom.
fun scrollUp() Move every row up one; the bottom row comes in blank.
fun get(x: Int, y: Int): Int The raw glyph index at (x, y), 0 outside the grid or in a blank cell.
fun prev(x: Int, y: Int): Int The glyph index at (x, y) as of the last snapshot.
fun prevColor(x: Int, y: Int): String The colour at (x, y) as of the last snapshot, "" if none.
fun colorAt(x: Int, y: Int): String The colour at (x, y) now, "" if none.
fun styleAt(x: Int, y: Int): String The per-cell CSS at (x, y) now, "" if none.
fun isShown(x: Int, y: Int): Boolean Whether (x, y) has been set since the last clear (and not scrolled out).
fun snapshot() Copy the glyphs and colours into the PREV buffer.
fun clear() Blank every cell, zero PREV, drop the CSS variables and home the cursor.
fun cssVar(name: String, value: String?) Set a CSS custom property on every cell, --spin to "30deg" say; null removes it.
fun glyphIndex(ch: String): Int The index of ch in the charset, 0 if it is not there.
val tableSize: Int The colour and style strings interned so far, "" included.
fun compact(limit: Int = TABLE_LIMIT): Boolean Rebuild the colour and style tables from the cells in use when they hold more than limit strings, so a program that makes a new colour every frame does not grow them without end. The panel then gets one full frame. Returns whether it rebuilt.
fun update(knownEpoch: Int, knownVersion: Int, knownColors: Int, knownStyles: Int): String The panel's poll: what changed since the panel's knownVersion of knownEpoch, given that it already holds knownColors colour and knownStyles style strings. Returns a JSON object: e epoch, v version, full, cols, rows, frame, glyphs (on a full frame), c0/c and s0/s (new table strings from those indices), css (the variables, when they changed), and d, the changed cells as flat [index, glyph, colour id, style id, …] with glyph -1 for a blank cell and the glyph already taken modulo the charset.

GlyphSound

class GlyphSound(val N: Int, val x: Int, val y: Int, val frame: Int)

What a glyph's sound lambda sees: the glyph index N being set, where, and on which tick.

Declaration Summary
val N: Int The glyph index being set (taken modulo the charset).
val x: Int The cell's column.
val y: Int The cell's row.
val frame: Int The tick.

CharGridSounds

class CharGridSounds(private val sounds: List<String>, private val sound: (GlyphSound.() -> String?)?, private val cells: Int, val maxVoices: Int = 24, private val play: (path: String, key: String) -> Unit)

Turns SETs into dough events: a per-glyph path table and/or a path lambda, and, in frame mode, the demo's auto-gain over one tick's SETs.

Declaration Summary
val maxVoices: Int The most distinct paths one frame-mode tick plays.
val hasSounds: Boolean Whether any glyph can sound at all.
fun resolve(N: Int, x: Int, y: Int, frame: Int): String? The path glyph N at (x, y) on tick frame plays, or null.
fun onSet(N: Int, x: Int, y: Int, frame: Int, immediate: Boolean) A SET of glyph N: played now if immediate, else counted towards this tick's flush.
fun voiceGain(): Double The demo's frame-mode gain for one voice: 1 / (enabled glyphs · √cells).
fun flush() Play this tick's collected paths and forget them.

CharGridScope

abstract class CharGridScope internal constructor(val grid: CharGridBuffer, internal val sounds: CharGridSounds)

What both char grid programs see: the size, the charset, the tick, and every verb except SET (which CharGridFrame and CharGridStream give their own timing).

Declaration Summary
val grid: CharGridBuffer The grid's cells; what the panel shows.
val COLS: Int Columns.
val ROWS: Int Rows.
val glyphs: String The charset: glyph index i shows as glyphs[i % glyphs.length].
var frame: Int The tick, from 0.
fun GET(x: Int, y: Int): Int The glyph index at (x, y) now; 0 outside the grid.
fun PREV(x: Int, y: Int): Int The glyph index at (x, y) on the previous tick; 0 outside the grid.
fun PREVC(x: Int, y: Int): String The colour at (x, y) on the previous tick; "" if none.
fun glyphIndex(ch: Char): Int The index of ch in glyphs, 0 if absent.
fun glyphIndex(ch: String): Int The index of ch (one character) in glyphs, 0 if absent.
fun setText(x: Int, y: Int, str: String, color: String? = null) Write str from (x, y) rightwards as glyphs of glyphs, stopping at the right edge. Does not wait.
fun print(str: String, color: String? = null) Write str at the cursor, \n starting a new line. Does not wait, in stream mode either.
fun newLine() Move the cursor to the start of the next line, scrolling at the bottom.
fun clearGrid() Blank the grid, zero PREV, drop CSS variables and home the cursor.
fun CURSOR(x: Int, y: Int) Move the cursor.
fun getCursor(): CharGridCursor Where the cursor is.
fun CSSVAR(name: String, value: Any?) Set CSS custom property name ("--spin") on every cell, for use in per-cell styles; null removes it.
fun rand(): Double A random number in 0 until 1 (the demo's Math.random()).
fun rand(n: Int): Int A random integer in 0 until n.

CharGridFrame

class CharGridFrame internal constructor(grid: CharGridBuffer, sounds: CharGridSounds) : CharGridScope(grid, sounds)

The receiver of charGrid's block, which runs once per tick.

Declaration Summary
fun SET(g: Int, color: String? = null, style: String? = null) Write glyph g at the cursor and advance it; color null keeps the cell's colour, style null clears its CSS.
fun SET(x: Int, y: Int, g: Int, color: String? = null, style: String? = null) Write glyph g at (x, y); nothing outside the grid. color null keeps the cell's colour, style null clears its CSS.

CharGridStream

class CharGridStream internal constructor(grid: CharGridBuffer, sounds: CharGridSounds, private val clock: CharGridClock, private val frames: Frames) : CharGridScope(grid, sounds)

The receiver of charStream's program: every SET waits for the next tick, as does waitFrame.

Declaration Summary
suspend fun waitFrame(): Int Wait for the next tick and return its number. PREV then holds the grid as it was at that tick.
suspend fun SET(g: Int, color: String? = null, style: String? = null) Write glyph g at the cursor, advance it, and wait for the next tick.
suspend fun SET(x: Int, y: Int, g: Int, color: String? = null, style: String? = null) Write glyph g at (x, y) and wait for the next tick.

ControlPresets.kt

src/jvmMain/kotlin/afield/ui/output/ControlPresets.kt — read the source

Used in: Visualizers, Displays, and Little Instruments

Top-level functions

Declaration Summary
fun Box.presetDirectory(channel: String = "controls"): Path Where a presetBar on channel keeps its keys by default: keys/<box id>/<channel> in the document's files — also the folder to hand keyframeBrowser, or to seed with key.save(...).
fun ControlGroupBuilder.presetBar(scope: KeyframeScope, directory: Path? = null, morphBetween: List<String> = emptyList(), morph: Boolean = true): PresetBar A preset strip for the controls of scope: a name field with store and recall, a button per saved key, and a presetMorphLabel slider blending across morphBetween (every saved key, in name order, when empty). Controls join by being declared with the keyframe* helpers on scope.
fun Box.recordControl(label: String, channel: String = "controls", curveName: String = label, tolerance: Double = 0.005): ControlRecording Arms a recording of the control labelled label in channel; see ControlRecording.
fun Box.playControl(label: String, curve: KeyframeEvaluator, time: Double, channel: String = "controls", curveY: KeyframeEvaluator? = null): Boolean Plays curve at time into the control labelled label: the control moves and its callback runs. Call it from an animation loop. A vec2 also needs curveY.
fun ControlGroupBuilder.recordToggle(label: String, curveName: String = label, tolerance: Double = 0.005) A record button for the control labelled label: press it, move the control, press it again, and the take lands in the keyframe curve curveName (opened below), ready to edit and to playControl back. A button rather than a checkbox because a checkbox's state is restored when the box runs again, which would arm a recording nobody asked for.
fun Box.midiLearn(channel: String = "controls"): ControlMidiMap The MIDI map for channel, one per box and channel however often the box runs; see ControlMidiMap.
fun ControlGroupBuilder.midiLearnButton(input: MidiInput? = null) A MIDI learn button: press it, touch a control, turn a knob.

Top-level values

Declaration Summary
const val presetMorphLabel The label of the blend slider presetBar adds. It is not a scope target, so no key holds it.

PresetBar

class PresetBar internal constructor(val scope: KeyframeScope, val directory: Path)

The keys a presetBar stores, in directory: one file per name, written by KeyframeScope.saveKey so each store adds to that key's version history and the folder opens in keyframeBrowser.

Declaration Summary
val names: List<String> Saved key names, sorted.
fun path(name: String): Path Where key name lives.
fun store(name: String, note: String? = null): KeyframeScope.Key Captures the scope as key name (a new version if it exists) and returns it.
fun recall(name: String): Boolean Applies the latest save of name; false if there is none.
fun morph(t: Double, between: List<String> = emptyList()): KeyframeScope.Key? Applies the blend at t (0..1) along between, in order — every saved key when empty — via KeyframeScope.interpolateAlong. Returns the applied key, or null with fewer than two keys.

ControlRecording

class ControlRecording internal constructor(val box: Box, val channel: String, val label: String, val curveName: String, val tolerance: Double)

A take of one control: every change a person makes to it while armed, with its time. stop bakes the take into a keyframes(...) editor named curveName, thinned so no key strays more than tolerance (a fraction of the control's range) from what was played.

Declaration Summary
val box: Box
val channel: String
val label: String
val curveName: String
val tolerance: Double
var armed
val sampleCount: Int Number of changes captured so far.
fun capture(atSeconds: Double, value: Double) Adds a sample by hand — what a test, or code generating a gesture, uses in place of a hand.
fun keys(): List<Keyframe> The take so far as keys (time in seconds from the first change). A vec2 gives x; use keysY for y.
fun keysY(): List<Keyframe> The y lane of a vec2 take.
fun stop(): KeyframeEvaluator Disarms and writes the take into the curve curveName (and $curveName.y for a vec2).

MidiBinding

data class MidiBinding(val key: String, val midiChannel: Int, val cc: Int)

A hardware controller cc on MIDI midiChannel (0-based) bound to the control with control key key.

ControlMidiMap

class ControlMidiMap internal constructor(val box: Box, val channel: String)

Knob bindings for one control group, saved with the box.

Declaration Summary
val isLearning: Boolean True while waiting for a control and a CC to bind.
val bindings: List<MidiBinding> The current bindings.
fun learn() Arms learning; see the class doc.
fun cancel() Disarms without binding.
fun bind(label: String, cc: Int, midiChannel: Int = 0): Boolean Binds cc on midiChannel to the control labelled label, in code.
fun unbind(label: String) Removes the binding for the control labelled label.
fun feed(midiChannel: Int, cc: Int, value: Int) A controller value arriving: learns it if armed, then drives whatever it is bound to.
fun attach(input: MidiInput) Listens to hardware input; detach by closing it.

LiveOutputIframe.kt

Script access to the iframe inside a box's output area as the Electron tab shows it: outputIframe resolves a box and channel to a LiveOutputIframe, whose calls (JavaScript evaluation, raw CDP, navigation) return futures so the runloop is not blocked while the tab answers.

src/jvmMain/kotlin/afield/ui/output/LiveOutputIframe.kt — read the source

Top-level functions

Declaration Summary
fun Box.outputIframe(channel: String): LiveOutputIframe The iframe in one of this box's own output areas.
fun Box.outputIframe(box: Box, channel: String): LiveOutputIframe The iframe in another box's output area; both boxes must belong to the same document.
fun Box.outputIframe(boxName: String, channel: String): LiveOutputIframe Resolve a box by uid or unique name in this box's document.

OutputIframeTarget

data class OutputIframeTarget(val boxUid: String, val channel: String)

A resolved box/output pair in the current document; the actual DOM iframe is found per call.

LiveOutputIframe

class LiveOutputIframe internal constructor(private val document: SessionInformation, val target: OutputIframeTarget)

Commands run against the visible Electron tab. Futures keep the Field runloop unblocked.

Declaration Summary
val target: OutputIframeTarget
fun evaluate(javascript: String, timeoutMillis: Long = 10_000): CompletableFuture<JsonElement>
fun cdp(method: String, params: JsonObject = JsonObject(emptyMap()), sessionId: String? = null, timeoutMillis: Long = 10_000): CompletableFuture<JsonElement> Raw CDP addresses the owning tab; pass a frame context/session id where the method needs one.
fun navigate(url: String, timeoutMillis: Long = 15_000): CompletableFuture<String> Change the actual iframe's src and complete after its load event.
fun screenshot(timeoutMillis: Long = 10_000): CompletableFuture<String> Screenshot the iframe's currently visible pixels in the live Electron tab.
fun inspect(selector: String = "", timeoutMillis: Long = 10_000): CompletableFuture<JsonElement> Current DOM source and document metadata, or matching elements for a CSS selector.
fun click(selector: String, timeoutMillis: Long = 10_000): CompletableFuture<JsonElement> Wait for a selector, then click the matching element in the live frame.
fun fill(selector: String, text: String, timeoutMillis: Long = 10_000): CompletableFuture<JsonElement> Wait for a text input, set its value, and emit input/change events for app frameworks.
fun waitFor(condition: String, timeoutMillis: Long = 10_000, pollMillis: Long = 25): CompletableFuture<Unit>

MarkdownAppend.kt

src/jvmMain/kotlin/afield/ui/output/MarkdownAppend.kt — read the source

Top-level functions

Declaration Summary
fun Box.markdownAppend(channel: String, line: String, maxEntries: Int = 200, maxChars: Int = 32_768, width: Int? = null, open: Boolean = true) Append markdown to a named output channel, retaining at most maxEntries entries and maxChars UTF-16 characters, including paragraph separators. Trim oldest entries first; a single oversized entry keeps its tail. Calling markdown replaces the text/history. History persists across runs, but a late append from an older run is ignored after a newer run has written this channel.

MarkdownOutput.kt

Markdown output for box output areas: markdown renders GitHub-flavoured markdown, with raw HTML, side-by-side :::row columns and {role:text} spans, into a styled card using Field's design tokens. FieldMarkdown holds the renderer and its full syntax; md and mdEscape escape a value for interpolation.

src/jvmMain/kotlin/afield/ui/output/MarkdownOutput.kt — read the source

Used in: Visualizers, Displays, and Little Instruments, Measuring dependence between two signals

Top-level functions

Declaration Summary
fun Box.markdown(channel: String, text: String, width: Int? = null, open: Boolean = true) Shows text, rendered as markdown, in this box's output area on channel.
fun Box.showRow(channel: String, parts: List<String>, width: Int? = null) Puts parts side by side in one card — the :::row block without the string plumbing.
fun Box.showRow(channel: String, vararg parts: String, width: Int? = null) showRow with the columns written inline rather than collected into a list.
fun Box.markdown(text: String, width: Int? = null, open: Boolean = true) Shows text as markdown on the default output channel. See Box.markdown.
fun mdEscape(value: Any?): String value as literal text inside markdown — the safe way to interpolate a computed value into a Box.markdown figure, and effectively required inside a table cell.

Top-level values

Declaration Summary
val Any?.md: String mdEscape as a property, so a value escapes itself where it is interpolated — "\| ${'$'}{worker.state.md} \|" rather than "\| ${'$'}{mdEscape(worker.state)} \|". The pair matches val Any?.h, which does the same for the HTML visualizer.

FieldMarkdown

object FieldMarkdown

Markdown for output areas — the compact way to put a figure in a box's output pane.

Declaration Summary
val ROLES Inline span roles: {accent:text} renders as <span class="field-md-accent">text</span>.
fun render(text: String): String Renders text to an HTML fragment — no card wrapper, so no styling. Usually you want card.
fun row(parts: List<String>): String parts as one :::row block — the source a Box.showRow card is built from, separate so a caller composing a larger figure can splice a row into the middle of it.
fun card(text: String, width: Int? = null): String Renders text wrapped in the field-md card that carries the stylesheet (see field.css).

NumericFormatting.kt

Fixed-point number formatting for string templates: x.f2 is x with two decimal places, from f0 to f9, always with a . decimal point regardless of the machine's locale.

src/jvmMain/kotlin/afield/ui/output/NumericFormatting.kt — read the source

Top-level values

Declaration Summary
val Number.f0: String Fixed-point text with zero fractional digits and a locale-independent decimal convention.
val Number.f1: String Fixed-point text with one fractional digit.
val Number.f2: String Fixed-point text with two fractional digits.
val Number.f3: String Fixed-point text with three fractional digits, useful inside string templates.
val Number.f4: String Fixed-point text with four fractional digits.
val Number.f5: String Fixed-point text with five fractional digits.
val Number.f6: String Fixed-point text with six fractional digits.
val Number.f7: String Fixed-point text with seven fractional digits.
val Number.f8: String Fixed-point text with eight fractional digits.
val Number.f9: String Fixed-point text with nine fractional digits.

OutputAreaReplay.kt

The panels a live box has shown, kept so that a page attaching after the box ran still sees them.

Every DefaultOutput holds one as retainedAreas: display(...) puts the rendered HTML in under its channel name, displayOpen and displayClose record whether a panel was open, and snapshot replays both as the set-output and output-control messages a client would have received live. A client asks for them with replay-output-areas on attaching, which is also what lets a capture run a box first and look at its output later.

Only the latest HTML per channel is held, bounded at 128 channels and eight million characters with the earliest channel dropped first, and none of it is written to disk.

src/jvmMain/kotlin/afield/ui/output/OutputAreaReplay.kt — read the source

callbacks.kt

Per-box callback tables that run when a box's frame, viewport, name or children change.

Box code adds a named entry to onFrameChange, onViewportChange, onNameChange or onTopologyChange. A frame or name callback receives the current and incoming value and returns the value to keep. A callback that throws is reported as an error on the box, and the remaining callbacks for that change are skipped.

src/jvmMain/kotlin/afield/ui/output/callbacks.kt — read the source

Top-level functions

Declaration Summary
fun onFrameChange(a: HasProperties, current: Rect, incoming: Rect): Rect
fun onViewportChange(a: HasProperties, current: Rect, incoming: Rect): Rect
fun onNameChange(a: HasProperties, current: String, incoming: String): String
fun onTopologyChange(a: Box, adding: Boolean, newChild: Box)

Top-level values

Declaration Summary
var Box.onFrameChange: MutableMap<String, (Box, Rect, Rect) -> Rect
var Box.onViewportChange: MutableMap<String, (Box, Rect, Rect) -> Unit
var Box.onNameChange: MutableMap<String, (Box, String, String) -> String
var Box.onTopologyChange: MutableMap<String, (Box, Boolean, Box) -> String

decorations.kt

Editor decorations driven from running code: RawDecoration marks a span of a box's source in Monaco, and the Marker extensions here — histogram, history, stats, liveNumber, toggle, beep, bop — attach live values and clickable widgets to the line that called them.

src/jvmMain/kotlin/afield/ui/output/decorations.kt — read the source

Top-level functions

Declaration Summary
fun setDecorations(box: Box, decor: List<RawDecoration>) Push a set of decorations for box to all active sessions.
fun Marker.locationInSource(then: String? = null): Location? Where this marker's __m("uid") text is in its box's code, or, with then, where then first follows it (.flipbox(), positioned the way locationForTextStringOnStack positions its end = true, offset = 1 results. Null when the text is not in the code.
fun <K, V> MutableMap<K, in V>.update(key: K, d: V, u: (V) -> V): V Update a map entry by computing or inserting a default, then applying u.
fun Marker.toggle(cb: () -> Unit) Attach a clickable "toggle" decoration to the call site.
fun Marker.beep(tick: Int = properties.update("beep", 0) { it + 1 }) Attach an alternating glyph decoration to the call site.
fun Marker.bop(text: String): BoppableString Creates a BoppableString wrapper to drive glyph decorations.
fun <T> Marker.histogram(value: T, dur: Int = 100): T Track a rolling histogram and emit it into the editor output.
fun <T> Marker.history(value: T, dur: Int = 100): T Track a rolling time series and emit it into the editor output.
fun <T> Marker.stats(value: T, dur: Int = 100): T Track rolling summary stats and emit a compact summary panel next to the call site.
fun locationForTextStringOnStack(code: String, box: Box? = null, st: Array<StackTraceElement> = Exception().stackTrace, end: Boolean = false, offset: Int = 0): Location? Locate the call site of code inside a box's current source text.
fun Marker.liveNumber(defaultValue: Number = 0.0): Double Returns a "live number" - a number that is read directly from the source code at this marker's location. When the source code changes, the returned value changes immediately without recompilation or reevaluation.

Top-level values

Declaration Summary
var Box.code_decorationState: String Serialized decoration payload cached on the box and pushed to clients.
var Box.code_decorationCallbackTable: MutableMap<String, () -> Unit Per-decoration callback table keyed by generated action ids.
val Box.decorationState: MutableMap<String, RawDecoration> High-level decoration state for a box.

RawDecoration

data class RawDecoration(val lineStart: Int, val colStart: Int, val lineEnd: Int, val colEnd: Int, val classname: String, val classDef: Map<String, String> = emptyMap(), val clickCallback: (() -> Unit)? = null)

A single Monaco decoration payload for a box.

Declaration Summary
val lineStart: Int
val colStart: Int
val lineEnd: Int
val colEnd: Int
val classname: String
val classDef: Map<String, String>
val clickCallback: (() -> Unit)?

Location

data class Location(val inside: Box, val property: String, val line: Int, val col: Int)

A precise source location inside a box property.

BoppableString

class BoppableString(val s: String, val inside: Marker) : () -> String

Wraps a string with a decoration emitter.

Declaration Summary
fun bop(index: Int, color: String = "white", filled: Boolean = true) Add a small glyph decoration at the call site.
override fun invoke(): String

LittleHistory

class LittleHistory

Minimal rolling history for histogram and time-series sketches.

Declaration Summary
var outputRateLimiter
fun add(v: Number?, size: Int = 100) Append a value and cap history length.
fun histogram(): String Render a histogram SVG for the current history.
fun history(): String Render a line chart SVG for the current history.
fun stats(): String Render a summary statistics panel (metrics + histogram + series).

outputs.kt

Output areas and controls for box code: HasOutput and the display channels, progress bars and withProgress, streamProcess for running external commands into a panel, the tree, column and logs embeds, and the controls DSL — slider, checkbox, stringInput, colorPicker, button, controlGroup and syncToProperty.

Each control renders into a named output channel (controls by default) and calls back into the box when the browser reports a change.

src/jvmMain/kotlin/afield/ui/output/outputs.kt — read the source

Used in: Visualizers, Displays, and Little Instruments, Box Output: print, display and channels, The keyframe editor: drawing a value over time

Top-level functions

Declaration Summary
fun Box.displayOpen(channel: String) Opens the output panel for channel, making an empty one first if the channel has none yet. Panels made by HasOutput.display start closed; see public/docs/box_output.md.
fun Box.displayClose(channel: String) Closes the output panel for channel to its title bar, keeping its content and size.
fun Box.displayClear(channel: String) Removes the output panel for channel and its content (it does not merely empty it). The next display into channel makes a new, closed panel at the end of the list. nested is never removed.
fun Box.displayClearAll() Removes every output panel this box has, except nested, which holds its nested boxes.
fun Box.progressBar(name: String, value: Double, details: String? = null)
fun Box.progressBarIndeterminate(name: String, details: String? = null) An indeterminate ("still working", no known total) progress bar named name.
fun Box.progressBarClear(name: String) Remove a progress bar named name from this box's output-area stack.
fun progressReporter(name: String = "progress", lingerMillis: Long = ProgressBars.lingerMillis, showDelayMillis: Long = ProgressBars.showDelayMillis): ProgressReporter A ProgressReporter for the box currently executing (via Box.context), or an inert no-op reporter when there is no active box or when ProgressBars.enabled is false.
fun <T> withProgress(name: String = "progress", lingerMillis: Long = ProgressBars.lingerMillis, showDelayMillis: Long = ProgressBars.showDelayMillis, block: (ProgressReporter) -> T): T Run block with a transient progress bar, always removing the bar afterwards. A bar that reached 100% inside block lingers as usual; one that didn't (early return or a thrown exception) is removed immediately. The reporter is inert outside a box context.
fun <T> sizedIterable(size: Int, iteratorFactory: () -> Iterator<T>): SizedIterable<T>
fun Box.streamProcess(name: String, command: List<String>, channel: String = name, workingDirectory: File? = null, env: Map<String, String> = emptyMap(), atError: suspend Box.(StreamProcessError) -> Unit = {}, atEnd: suspend Box.(StreamProcessEnd) -> Unit = {}): Frames Launches an external process and streams stdout/stderr into an output area.
fun Box.streamProcessFireAndForget(name: String, command: List<String>, channel: String = name, workingDirectory: File? = null, env: Map<String, String> = emptyMap(), strand: String = streamProcessStrand(name, channel), atError: suspend Box.(StreamProcessError) -> Unit = {}, atEnd: suspend Box.(StreamProcessEnd) -> Unit = {}) Starts streamProcess immediately, without requiring the returned Frames to be the launched value of a box. Reusing the same strand cancels the previous process.
fun Box.streamProcessFireAndForget(name: String, vararg command: String, channel: String = name, workingDirectory: File? = null, env: Map<String, String> = emptyMap(), strand: String = streamProcessStrand(name, channel), atError: suspend Box.(StreamProcessError) -> Unit = {}, atEnd: suspend Box.(StreamProcessEnd) -> Unit = {})
fun Box._printHere(line: Int, vararg a: Any?)
fun Box.macro_printHere(contents: List<String>): Insert?
fun Box._printlnHere(line: Int, vararg a: Any)
fun Box.marco_printlnHere(contents: List<String>): Insert?
fun Box.tree(channel: String = "debug", a : Any?, maxHeight: Int = 520, includePrivate: Boolean = false, width: Int? = null) Shows a in output channel as an expandable tree: maps, lists, arrays, data classes and plain objects open lazily, one page of afield.app.server.Inspector.DEFAULT_PAGE children at a time, and each row names its type and a one-line summary. Rows that are open follow the value as it changes (about once a second). Keys: arrows to move and open, / to filter, c to copy the value, p to copy the row's path, r to refresh.
fun Box.column(channel: String = "debug", a : Any?, maxHeight: Int = 420, includePrivate: Boolean = false, width: Int? = null, open: String? = null) Shows a in output channel as Finder-style columns: choosing a row opens its contents in the next column. Same values, paging, live update and keys as tree, plus ←/→ between columns.
fun Box.remoteAr(channel: String = "remote-ar", width: Int = 960, height: Int = 540, debug: Boolean = true, selfTest: Boolean = false) Embeds Field's RemoteJS / WebXR browser head in this box's output pane and opens it, so the live A-Frame / Three.js scene renders right on the canvas beside the box. This is the same page served at /extensions/remote_ar/index.html; it connects back to the Field server over WebSocket and executes Javascript RemoteJS boxes. See docs/ar_reference.md.
fun Box.keyframeBrowser(root: Path, pattern: String = everyKeyGlob, channel: String = "keyframes", scope: KeyframeScope? = null, width: Int = 900, height: Int = 460) Browse a directory of saved keys in this box's output area.
fun Box.keyframeBrowser(root: String, pattern: String = everyKeyGlob, channel: String = "keyframes", scope: KeyframeScope? = null, width: Int = 900, height: Int = 460) keyframeBrowser against a directory named as a string, resolved through the document's files.
fun Box.logs(channel: String = "internal logging", filter: LogStream.Filter = LogStream.Filter(), latestOnly: Boolean = false, subId: String = "logs-${UUID.randomUUID()}")
fun Box.logs(channel: String = "debug", latestOnly: Boolean = false, subId: String = "logs-${UUID.randomUUID()}", build: LogFilterBuilder.() -> Unit)
fun Box.transcriptEntries(): List<LinkedHashMap<String, Any?>>
fun Box.transcriptPayload(): LinkedHashMap<String, Any?>
fun Box.transcriptHtml(): String
fun Box.transcript(channel: String = "transcript")
fun Box.slider(label: String, min: Double = 0.0, max: Double = 1.0, step: Double = 0.01, initialValue: Double = (min + max) / 2, channel: String = "controls", law: ControlLaw = ControlLaw.LINEAR, lawExponent: Double = 2.0, lawCenter: Double = (min + max) / 2, significantDigits: Int = defaultSignificantDigits(law), onUpdate: (Double) -> Unit) Displays a numeric slider in an output disclosure and calls onUpdate whenever its value changes.
fun Box.sliders(channel: String = "controls", vararg configs: Pair<String, SliderConfig>) Displays several SliderConfig sliders together under one disclosure channel.
fun Box.vec2Control(label: String, initialValue: Vector2f = Vector2f(0.0f, 0.0f), xRange: Pair<Double, Double> = -1.0 to 1.0, yRange: Pair<Double, Double> = -1.0 to 1.0, step: Double = 0.01, channel: String = "controls", onUpdate: (Vector2f) -> Unit)
fun Box.checkbox(label: String, initialValue: Boolean = false, channel: String = "controls", onUpdate: (Boolean) -> Unit)
fun Box.stringInput(label: String, initialValue: String = "", channel: String = "controls", onUpdate: (String) -> Unit)
fun storeFileUploadData(controlId: String, files: List<FileUploadEntry>)
fun getFileUploadData(controlId: String): List<FileUploadEntry>?
fun Box.fileUpload(label: String = "Upload", accept: String = "", multiple: Boolean = false, channel: String = "controls", onUpload: (List<FileUploadEntry>) -> Unit)
fun Box.colorPicker(label: String, initialValue: Vector4f = Vector4f(1.0f, 1.0f, 1.0f, 1.0f), channel: String = "controls", onUpdate: (Vector4f) -> Unit)
fun Box.button(label: String, channel: String = "controls", onUpdate: (Boolean) -> Unit) Displays a momentary button in an output disclosure — down while it is held, up when it is let go.
fun Box.controlGroup(channel: String = "controls", block: ControlGroupBuilder.() -> Unit) Builds a compact set of related controls in one output disclosure.
fun Box.controlGroup(channel: String = "controls", columns: Int, block: ControlGroupBuilder.() -> Unit) A control surface with up to columns responsive columns. Use beside a wide mesh or plot to keep controls visible without a long vertical scroll. Narrow outputs collapse to fewer columns; the ordinary two-argument overload retains its existing single-column layout. Control identity, cached values, callbacks and tablet routing are unchanged.
fun Box.syncToProperty(propertyName: String, initialValue: Any, channel: String = "controls", label: String = propertyName) Shows a control bound to the box property propertyName and marks that property persistent.
fun Box.syncToProperty(propertyName: String, initialValue: Double, min: Double, max: Double, step: Double = 0.01, channel: String = "controls", label: String = propertyName, law: ControlLaw = ControlLaw.LINEAR) syncToProperty for a number over min..max: a slider whose value lives in the box property propertyName, persists with the document, and survives re-runs. A double-click on the track returns to initialValue.
fun Box.showLayers()
fun <T> Box.tqdm(items: SizedIterable<T>, name: String = "progress"): Iterable<T>+2 overloads · also List<T>, Iterable<T>
fun Box.tqdm(items: IntRange, name: String = "progress"): Iterable<Int>+1 overload · also Int

HasOutput

interface HasOutput

Receives text, display, and error output for a box or runtime.

Declaration Summary
fun print(message: Any?) Emits message without appending a newline.
fun println(message: Any?) Emits message followed by a newline.
fun display(message: Any?) Displays message on the default channel, output.
fun display(channel: String, message: Any?) Displays message in the output panel named channel, replacing what the panel held. A String is inserted as HTML; anything else is rendered with .h first. A channel whose name starts with * is a one-line row rather than a panel.
fun displayError(message: Any?, property: String = "code", line: Int = -1) Paints message as an error diagnostic on property at line of this box; a null message at line -1 clears the errors. Named displayError rather than error so that, inside a box and in Box extensions, error("...") is Kotlin's and throws (?: error(...) narrows as it should).

DefaultOutput

class DefaultOutput : HasOutput

Declaration Summary
var proxy: (Any?, Boolean, Number) -> Unit
var displayProxy: (String, Any?) -> Unit
var errorProxy: (Any?, String, Int) -> Unit
var warningProxy: (String, Int, Int, Int, Int) -> Unit A compiler warning for the run being configured: message, line, start column, end line, end column (1-based, -1 unknown).
override fun print(message: Any?)
override fun display(channel: String, message: Any?)
override fun display(message: Any?)
override fun println(message: Any?)
override fun displayError(message: Any?, property: String, line: Int)

ProgressBars

object ProgressBars

Global switch for the transient progress bars offered by progressReporter and Box.tqdm.

Declaration Summary
var enabled: Boolean
var lingerMillis: Long
var showDelayMillis: Long

ProgressReporter

class ProgressReporter internal constructor(private val box: Box?, private val name: String, private val lingerMillis: Long = ProgressBars.lingerMillis, showDelayMillis: Long = ProgressBars.showDelayMillis) : AutoCloseable

A transient tqdm-style progress bar bound to whichever Box is currently executing.

Declaration Summary
val active: Boolean True when attached to a live box and not yet finished — i.e. it may still draw.
fun update(fraction: Double, details: String? = null) Draw the bar at fraction of the way done (clamped to 0..1). Reaching 1.0 completes it.
fun update(done: Long, total: Long, details: String? = null) Draw the bar at done/total, with a default tqdm-style rate/ETA line if details is null.
fun step(total: Long, by: Long = 1L, details: String? = null) Advance by by out of total and redraw.
fun pulse(details: String? = null) Report indeterminate activity — one "beat" of work whose total is unknown (e.g. a reduce that runs to a fixpoint). Shows an animated bar with a step count and elapsed time rather than a fraction. Never auto-completes; the bar is removed by finish/close. Like every method here, it is a no-op outside a box, while disabled, or during the grace period.
fun finish() Remove the bar immediately. Idempotent, and a no-op when inert or already completed — so an outer finally won't cut short the linger of a bar that finished on its own. This is the abort path: a bar that never reached 100% is removed at once. Sends nothing if the bar was never shown (i.e. it lived and died entirely inside the grace period).
override fun close()

SizedIterable

interface SizedIterable<out T> : Iterable<T>

Declaration Summary
val size: Int

StreamProcessErrorKind

enum class StreamProcessErrorKind

Constants: NO_COMMAND, START_FAILED, STDERR, NON_ZERO_EXIT.

StreamProcessError

data class StreamProcessError(val kind: StreamProcessErrorKind, val name: String, val label: String, val command: List<String>, val channel: String, val message: String, val exitCode: Int? = null, val throwable: Throwable? = null)

Declaration Summary
val kind: StreamProcessErrorKind
val name: String
val label: String
val command: List<String>
val channel: String
val message: String
val exitCode: Int?
val throwable: Throwable?

StreamProcessEnd

data class StreamProcessEnd(val name: String, val label: String, val command: List<String>, val channel: String, val exitCode: Int, val terminated: Boolean = false)

Declaration Summary
val name: String
val label: String
val command: List<String>
val channel: String
val exitCode: Int
val terminated: Boolean

LogFilterBuilder

class LogFilterBuilder

Declaration Summary
var channels: MutableList<String>
var cat: String?
var matching: String?
var regex: String?
var blockId: String?
var includeBlockChildren: Boolean
var sinceEpochMs: Long?
var afterSeq: Long?
var limit: Int
var tail: Boolean
fun channel(vararg tokens: String)

SliderConfig

data class SliderConfig(val label: String, val min: Double = 0.0, val max: Double = 1.0, val step: Double = 100.0, val initialValue: Double = 0.0, val onUpdate: (Double) -> Unit)

One entry in a sliders group: a labelled numeric range plus its update callback.

Declaration Summary
val label: String
val min: Double
val max: Double
val step: Double
val initialValue: Double
val onUpdate: (Double) -> Unit

MeterHandle

class MeterHandle internal constructor(val controlId: String, internal val targetDocument: String?)

A live handle on one meter in a control group — the thing a box holds in order to show a level.

Declaration Summary
val controlId: String The control id this handle drives; useful when a box wants to log what it is feeding.
val counts: Pair<Long, Long> How many updates passed this meter's rate gate, and how many it coalesced locally.
fun set(value: Double): Boolean Shows value on this meter, if the rate limit and the previous value allow it.

ControlValueHandle

class ControlValueHandle<T> internal constructor(val controlId: String, private val targetDocument: String?, private val read: (ControllerControl) -> T?, private val write: (ControllerControl, T) -> ControllerControl, private val copy: (T) -> T = { it })

A writable view of one cached controlGroup value.

Declaration Summary
val controlId: String Stable id derived from box, channel, control type, label and occurrence.
fun set(value: T): Boolean Show value everywhere this control appears. Returns false when it was already current.

ControlGroupBuilder

class ControlGroupBuilder(private val box: Box, private val channel: String)

Declaration Summary
fun afield.animation.keyframes.KeyframeScope.controls() Add every control declared on this scope with var size by keys.slider(...) (and vec2, color, checkbox, text), in declaration order — see KeyframeControl.
fun slider(label: String, min: Double = 0.0, max: Double = 1.0, step: Double = 0.01, initialValue: Double = (min + max) / 2, law: ControlLaw = ControlLaw.LINEAR, lawExponent: Double = 2.0, lawCenter: Double = (min + max) / 2, significantDigits: Int = defaultSignificantDigits(law), onUpdate: (Double) -> Unit): ControlValueHandle<Double> Adds a bounded numeric value to this control group.
fun colorPicker(label: String, initialValue: Vector4f = Vector4f(1.0f, 1.0f, 1.0f, 1.0f), onUpdate: (Vector4f) -> Unit): ControlValueHandle<Vector4f> Adds an RGBA color editor to this control group.
fun vec2Control(label: String, initialValue: Vector2f = Vector2f(0.0f, 0.0f), xRange: Pair<Double, Double> = -1.0 to 1.0, yRange: Pair<Double, Double> = -1.0 to 1.0, step: Double = 0.01, onUpdate: (Vector2f) -> Unit): ControlValueHandle<Vector2f> Adds an XY pad for editing a related pair of numeric values.
fun button(label: String, onUpdate: (Boolean) -> Unit) Adds a momentary button — down while it is held, up when it is let go.
fun meter(label: String, min: Double = 0.0, max: Double = 1.0, initialValue: Double = min, law: ControlLaw = ControlLaw.LINEAR, lawExponent: Double = 2.0, lawCenter: Double = (min + max) / 2, significantDigits: Int = defaultSignificantDigits(law), source: (() -> Double)? = null): MeterHandle Adds a read-only meter: a level you can see, rather than a number you have to print.
fun checkbox(label: String, initialValue: Boolean = false, onUpdate: (Boolean) -> Unit): ControlValueHandle<Boolean> Adds a boolean toggle to this control group.
fun text(label: String, initialValue: String = "", onUpdate: (String) -> Unit): ControlValueHandle<String> Adds a small text editor to this control group.
fun stringInput(label: String, initialValue: String = "", onUpdate: (String) -> Unit) Adds a small text editor. This is the descriptive alias for text.

replacements.kt

A small {{placeholder}} templating system: filter replaces each placeholder from knownValues or templates, and an unknown one becomes empty text.

src/jvmMain/kotlin/afield/ui/output/templates/replacements.kt — read the source

Top-level functions

Declaration Summary
fun replaceTemplate(input: String, replacer: (placeholder: String, wholeText: String) -> String): String
fun filter(source: String)

Top-level values

Declaration Summary
val templates Template functions by placeholder name; each receives the placeholder and the whole text.
val knownValues

terminalOutput.kt

A real terminal in a box's output area: terminal starts a process in a panel and returns a FieldTerminal that box code can type into, read, and kill. The process outlives its panel, which re-attaches with its scrollback.

src/jvmMain/kotlin/afield/ui/output/terminalOutput.kt — read the source

Used in: A Terminal in a Box

Top-level functions

Declaration Summary
fun Box.terminal(channel: String = "terminal", command: List<String> = listOf(System.getenv("SHELL")?.takeIf { it.isNotBlank() } ?: "/bin/zsh", "-l"), directory: Path? = null, width: Int = 760, height: Int = 420, keep: Boolean = false): FieldTerminal Puts a real terminal — xterm.js on a pseudo-terminal — in this box's output area on channel.
fun Box.terminal(channel: String, commandLine: String, directory: Path? = null, width: Int = 760, height: Int = 420, keep: Boolean = false): FieldTerminal terminal running one command line through your shell, e.g. terminal("build", "./gradlew build").

FieldTerminal

class FieldTerminal internal constructor(val id: String, startAt: Long = 0)

A terminal started by terminal: the process keeps running when its panel is closed or the page reloads, and the panel re-attaches with its scrollback.

Declaration Summary
fun write(text: String): Boolean Types text into the terminal; add "\n" (or "\r") to submit a line. False once it has exited.
fun send(line: String): Boolean Types line and a return.
fun kill() Ends the process and everything it started.
val running: Boolean
val exitCode: Int? The exit code, or null while it runs.
val info: TerminalInfo?
suspend fun awaitReady(timeoutMs: Long = 10_000) Suspends until the program has printed something and gone quiet (a shell showing its prompt), or has exited. send already waits for this before typing.
suspend fun awaitExit(timeoutMs: Long = Long.MAX_VALUE): Int Suspends until the process exits, and returns its exit code.
suspend fun awaitOutput(pattern: Regex, timeoutMs: Long = 30_000): MatchResult Suspends until pattern appears in the terminal's output, and returns the match. Colour and cursor escapes are stripped first, so match the text you see.
suspend fun awaitOutput(text: String, timeoutMs: Long = 30_000): MatchResult awaitOutput for a literal string.
suspend fun sendAndAwait(line: String, pattern: Regex, timeoutMs: Long = 30_000): MatchResult Types line and a return, then suspends until pattern appears in the reply. The reply starts after the terminal's echo of line, so \d+ after sending 6 * 7 matches 42.
override fun toString()

AudienceBuilder.kt

The HTML an audience slide is written in: kotlinx.html's builders, with scoped Markdown, stylesheets and two picture layouts added.

SlideStepBuilder.audience runs its block against an AudienceBuilder, which is a kotlinx.html DIV carrying the slide's class, so every standard tag works inside it, along with Kotlin's own loops, functions and completion. Text added with + is escaped by the library; "# Heading".md renders Markdown instead, and the unary plus on the AudienceMarkdown it returns inserts the generated tags unescaped. That pair is how one slide mixes written prose with built HTML.

Use AudienceBuilder.div rather than kotlinx.html's own div for a nested container, since only this one keeps the Markdown syntax available inside it. AudienceBuilder.stylesheet links a CSS file from the document's files directory; the audience fetches it past the browser cache each time a slide is drawn, so an edit to it appears on Refresh slide. AudienceBuilder.fullScreenImage and AudienceBuilder.imageBeside are the picture layouts. Both want alt text and take an optional visible credit, and AudienceImageFit chooses between containing the whole picture and cropping it to fill.

src/jvmMain/kotlin/afield/ui/slides/AudienceBuilder.kt — read the source

AudienceMarkdown

class AudienceMarkdown internal constructor(internal val html: String)

A rendered Markdown fragment, inserted with unary plus inside AudienceBuilder.

AudienceImageFit

enum class AudienceImageFit(internal val css: String)

How an audience image fits its available space.

Constants: Contain, Cover.

AudienceBuilder

class AudienceBuilder internal constructor(consumer: TagConsumer<*>, classes: String = "field-slide") : DIV(attributesMapOf("class", classes), consumer)

Standard HTML slide content, with scoped Markdown, stylesheets and image layouts.

Declaration Summary
val String.md: AudienceMarkdown Render an indented Markdown block; ordinary +String still escapes text as HTML.
operator fun AudienceMarkdown.unaryPlus() Insert a rendered Markdown fragment without escaping its generated tags.
fun div(classes: String? = null, block: AudienceBuilder.() -> Unit = {}) A nested HTML container that keeps the slide's scoped Markdown syntax available.
fun stylesheet(source: String) Load a stylesheet relative to this document's files/ directory; Refresh slide reloads it.
fun fadeIn(durationMs: Int = 350, delayMs: Int = 0, block: AudienceBuilder.() -> Unit) Fade this text, image or group in on forward entry; Back shows its completed state.
fun fullScreenImage(source: String, alt: String, citation: String = "", fit: AudienceImageFit = AudienceImageFit.Contain) Fill the audience display with an image and an optional visible credit.
fun imageBeside(source: String, alt: String, citation: String = "", imageOnLeft: Boolean = true, fit: AudienceImageFit = AudienceImageFit.Contain, block: AudienceBuilder.() -> Unit) Put an image beside ordinary HTML or Markdown content, with its own credit.

LessonCheckpoint.kt

A reset button for a lesson: capture a box's source before an audience changes it, and restore it from a later slide.

capture records the named boxes' local text properties (code by default) in memory, and restore stops those boxes' tracked jobs and writes the text back. The scope is narrow: unlisted boxes, other properties, output, files and anything a run created outside a text property are untouched, so a slide that resets an experiment runs the setup again afterwards rather than waiting for the old output to reappear.

A read-only document refuses the restore, and a captured box since removed from the document fails it rather than being recreated.

presentation_studio.md pairs this with a checkpoint(...) step, which is how the templates/presentation document uses it.

src/jvmMain/kotlin/afield/ui/slides/LessonCheckpoint.kt — read the source

LessonCheckpoint

class LessonCheckpoint private constructor(private val entries: List<Entry>)

An in-memory baseline of explicitly named local text properties; never a snapshot of runtime objects.

Declaration Summary
fun restore() Stop the captured boxes and restore their text. Run setup explicitly afterwards to recreate output.

Presentation.kt

Giving a talk from a Field document: Box.presentation builds the same steps a slideShow does, and each step can carry a slide for a separate audience display.

A step's audience surface is HTML, a real webpage, a YouTube embed, or another document in the workspace. Write the HTML with SlideStepBuilder.audience for kotlinx.html's type-safe builders (see AudienceBuilder), with SlideStepBuilder.audienceHtml for a fragment you already have, or with SlideStepBuilder.audienceMarkdown. SlideStepBuilder.audienceWebpage and SlideStepBuilder.audienceYouTube put a page or a video in an isolated native Electron view, and SlideStepBuilder.audienceDocument opens another workspace document with its own editor, outputs and guide. A step with no audience surface shows its note HTML, templates and all, so a guide written for a reader presents without being rewritten.

SlideShowInstance.present starts the talk. Each step publishes a presentation-state message to the windows showing the host box's document, and Field's Electron app opens the audience and presenter windows from it; in a browser the document and its guides still work, but there is no separate display. Running the host box again rebuilds the talk and keeps the current step where it can. Anything bound for Electron is checked before it gets there: a webpage needs an HTTP or HTTPS URL with a host, a document a plain name in the current workspace, a video the eleven-character YouTube id, and a fractional rect has to sit inside the audience window.

Presenting with Field is the guide to writing one.

src/jvmMain/kotlin/afield/ui/slides/Presentation.kt — read the source

Used in: Presenting with Field

Top-level functions

Declaration Summary
fun Box.presentation(title: String = name.ifBlank { "Field presentation" }, transitionMs: Int = 450, init: SlideShowBuilder.() -> Unit): SlideShowInstance Build a presentation using the existing guide steps, callbacks, notes and annotations. Call SlideShowInstance.present to open its audience display in Field's Electron app. Running the same box again preserves the current step when possible.
fun SlideShowInstance.present() Open this guide as a presentation, using note HTML for steps without an explicit audience surface.
fun SlideStepBuilder.audience(block: AudienceBuilder.() -> Unit): AudienceContent Standard kotlinx.html builders; text is escaped by the library. No JavaScript build is needed.
fun audienceFragment(block: AudienceBuilder.() -> Unit): AudienceContent Reusable slide content. Keep one copy of the markup and shared image paths across slides.
fun SlideStepBuilder.audience(content: AudienceContent) Show a reusable HTML fragment without changing its content.
fun SlideStepBuilder.audienceOver(previous: AudienceContent, dimPrevious: Double = 0.25, block: AudienceBuilder.() -> Unit) Repeat HTML content dimmed behind a new layer; native webpages and documents stay separate surfaces.
fun SlideStepBuilder.reveal(title: String, dimPrevious: Double = 0.25, block: AudienceBuilder.() -> Unit) A cue that retains the preceding HTML view underneath a new layer.
fun SlideStepBuilder.audienceHtml(html: String) Set an HTML fragment for the audience. Use audience for type-safe, escaped HTML construction.
fun SlideStepBuilder.audienceMarkdown(markdown: String) Render Markdown on the audience display using the guide's existing Markdown renderer.
fun SlideStepBuilder.audienceWebpage(url: String, rect: List<Double>? = null) Show a real browser page in an isolated native Electron view, optionally within a fractional rect.
fun SlideStepBuilder.audienceYouTube(videoId: String, rect: List<Double>? = null) Show a YouTube embed; the video must permit embedding.
fun SlideStepBuilder.audienceDocument(document: String, macro: String? = null, interactiveGuide: Boolean = true, rect: List<Double>? = null) Show an existing document in the current workspace, with its normal Field editor and outputs. A named saved macro and the document's interactive guide are explicit presenter controls.

PresentationOptions

data class PresentationOptions(val title: String, val transitionMs: Int = 450)

Options for showing a guide on a separate audience display.

PresentationSearch.kt

Reduces a slide to the plain text a presenter can search, by reading the audience HTML the talk would actually show.

The slideshow calls text for every step when a client asks for search text, and caches the result for the built talk. It parses the HTML with the JDK's own parser, drops <style> and <script> subtrees, keeps image alt text, and adds the one identifying string of the other audience types: a document name, or a webpage or video URL. A step with no audience content at all falls back to its presenter note. The result is capped at 16 384 characters.

Indexing stays on authored content. The presenter-note fallback is taken as written rather than with its template functions evaluated, because those can run user code, and a step whose audience provider throws (a slide file not written yet) is skipped so the rest of the talk still opens.

src/jvmMain/kotlin/afield/ui/slides/PresentationSearch.kt — read the source

PresentationSlides.kt

The on-disk format for a talk's editable slides, and the two slides(...) builders that read a deck into a slideshow.

A deck is a files/<name>.slides.json inside the presentation document: a schema string, a stylesheet path, and a list of slides carrying Markdown for the audience, presenter notes, an optional section path and an optional list of reveals. validated() bounds every field of it (at most 1000 uniquely identified slides, 100 000 characters of Markdown each, 100 reveals), and both reading and writing run it.

PresentationSlideFiles is the file side, shared by Kotlin box code and by AI4's slide tools in PresentationTools. A write names the revision it read, so a deck edited underneath a caller is refused rather than overwritten, and every path is checked to stay inside the document's files/ directory.

A deck carries Markdown and no code; a live action stays an explicit Kotlin guide step.

src/jvmMain/kotlin/afield/ui/slides/PresentationSlides.kt — read the source

Top-level functions

Declaration Summary
fun SlideShowBuilder.slides(path: String) Insert editable Markdown slides at this point in the talk. Refresh reloads existing slide bodies from disk.
fun SlideShowBuilder.slides(file: File) Load a specific slide file; relative media still resolves in the presentation document's files directory.

PresentationSlides

data class PresentationSlides(val schema: String = "field.presentation.slides.v1", val stylesheet: String = "presentation.css", val slides: List<PresentationSlide> = emptyList())

Editable content for a talk. Live actions remain explicit Kotlin guide steps.

Declaration Summary
val schema: String
val stylesheet: String
val slides: List<PresentationSlide>
fun validated(): PresentationSlides

PresentationSlide

data class PresentationSlide(val id: String, val title: String, val markdown: String, val notes: String = "", val section: List<String> = emptyList(), val cues: List<PresentationReveal> = emptyList())

Declaration Summary
val notes: String

PresentationReveal

data class PresentationReveal(val title: String, val markdown: String, val retainPrevious: Boolean = false, val previousOpacity: Double = 0.25, val fadeMs: Int = 0)

A content-only reveal replaces or layers the slide body and never executes code.

Declaration Summary
val retainPrevious: Boolean

PresentationSlideFiles

class PresentationSlideFiles(private val documentDirectory: File)

Document-local files shared by the Kotlin builder and AI4's revision-checked slide tools.

Declaration Summary
data class Snapshot(val revision: String, val deck: PresentationSlides)
fun read(path: String): Snapshot
fun write(path: String, revision: String, deck: PresentationSlides): Snapshot Empty revision creates a new file; every replacement must name the exact revision just read.

SlideShow.kt

SlideShow - A DSL for creating guided tours through Field documents

This system supports creating interactive guides with: - Step-by-step navigation - Custom link callbacks that execute Kotlin code - Template system for displaying live data - Annotations and visual highlighting

Example usage:

// Set up properties that templates can reference
var counter = 0
this.properties["counter"] = counter

slideShow {
    step("Introduction") {
        note("""
            <h2>Welcome!</h2>
            <p>Counter value: {{counter}}</p>
            <p><a href="#custom:increment">Click to increment</a></p>
            <p><a href="#slideshow:next">Continue →</a></p>
        """)

        // Register a callback for the custom link
        linkCallback("increment") {
            counter++
            here.properties["counter"] = counter
            // Refresh to show new value
            show.goToStep(show.currentStepIndex)
        }

        selectBox(myBox)
    }
}

Custom Links: Use #custom:callbackName in href attributes, then register with linkCallback("callbackName") { ... }

Templates: Use {{propertyName}} to reference box properties or Kotlin variables in the guide's scope

Navigation Links: - #slideshow:next - Go to next step - #slideshow:previous - Go to previous step - #slideshow:home - Go to first step - #slideshow:stop - Stop the slideshow

src/jvmMain/kotlin/afield/ui/slides/SlideShow.kt — read the source

Used in: Interactive Guides, Presenting with Field

Top-level functions

Declaration Summary
fun Box.slideShow(init: SlideShowBuilder.() -> Unit): SlideShowInstance Build a slideshow, store it on this box, and return it. Call SlideShowInstance.start to show it.
fun Box.selectAndEdit(property: String = "code", line: Int? = null, highlight: Int? = null) Quick select and edit a box with optional line navigation
fun SlideStepBuilder.markdownNote(markdown: String, persistent: Boolean = false, anchorBox: Box? = null, animated: Boolean = false)
fun Box.showNote(html: String, persistent: Boolean = false, anchorBox: Box? = this, animated: Boolean = false, annotations: List<SlideAnnotation> = emptyList()) Show a slide note
fun Box.showMarkdownNote(markdown: String, persistent: Boolean = false, anchorBox: Box? = this, animated: Boolean = false, annotations: List<SlideAnnotation> = emptyList())
fun hideNote() Hide slide notes
fun Box.withChat(id: String? = null) Add a shared chat interface to this box's output area.

Top-level values

Declaration Summary
var Box.slideShow: SlideShowInstance?

SlideShowBuilder

class SlideShowBuilder(val hostBox: Box, private val presentationOptions: PresentationOptions? = null)

Builder for creating slideshows

Declaration Summary
fun step(title: String, id: String? = null, init: SlideStepBuilder.() -> Unit) Defines a step in the slideshow
fun section(title: String, init: SlideShowBuilder.() -> Unit) Group steps in the presenter outline. Sections may nest; playback stays in source order.
fun slideshowId(id: String) Set a custom slideshow ID (optional, will generate one if not set)
fun build(): SlideShowInstance

SlideStepBuilder

class SlideStepBuilder(val title: String, val hostBox: Box)

Builder for individual slide steps

Declaration Summary
fun cue(title: String, init: SlideStepBuilder.() -> Unit) A reveal within this slide. onEnter runs once on forward entry; Back only changes the view.
fun checkpoint(title: String, reset: suspend () -> Unit) Explicit presenter reset. Restoring declared state is separate from navigation.
fun note(html: String, persistent: Boolean = false, anchorBox: Box? = null, animated: Boolean = false) Sets the HTML note to display for this step
fun anchorNoteTo(box: Box) Anchors the note to a specific box (defaults to the slideshow host box)
fun selectBox(box: Box, property: String = "code", line: Int? = null, highlight: Int? = null) Selects a box and optionally scrolls to a specific line
fun selectBoxByName(name: String, property: String = "code", line: Int? = null, highlight: Int? = null) Selects a box by name (searches through children recursively) and optionally scrolls to a specific line
fun onEnter(action: suspend () -> Unit) Code to execute when entering this step
fun onExit(action: suspend () -> Unit) Code to execute when leaving this step
fun linkCallback(name: String, action: suspend LinkCallbackContext.() -> Unit) Register a callback for a custom link in the note HTML. Use in markdown as: <a href="#custom:linkName">Click me</a> or in markdown link syntax: [Click me](#custom:linkName)
fun withChat(id: String? = null) Enable chat/discussion for this slide. When enabled, a collapsible "Discussion" section appears below the slide content where users can post messages and questions.
fun annotate(id: String, targetBox: Box, color : String = "rgba(255, 180, 100, 0.8)", strokeWidth: Double = 2.0, dashed: Boolean = true) Add an annotation line from the guide text to a box
fun annotateToLine(id: String, targetBox: Box, property: String = "code", line: Int, color: String = "rgba(128, 255, 164, 0.7)", strokeWidth: Double = 2.0, dashed: Boolean = false) Add an annotation line from the guide text to a specific line in a box's editor
fun build(): SlideStep

LinkCallbackContext

class LinkCallbackContext(val here: Box, val show: SlideShowInstance)

Context provided to link callbacks

Declaration Summary
val here: Box
val show: SlideShowInstance

SlideStep

data class SlideStep(val title: String, val noteHtml: String?, val noteAnchorBox: Box?, val targetBox: Box?, val targetProperty: String, val targetLine: Int?, val highlightLine: Int?, val persistent: Boolean, val animated: Boolean, val withChat: Boolean, val customChatId: String?, val annotations: List<SlideAnnotation>, val onEnter: (suspend () -> Unit)?, val onExit: (suspend () -> Unit)?, val linkCallbacks: Map<String, suspend LinkCallbackContext.() -> Unit> = emptyMap(), val audience: AudienceContent? = null, val sectionPath: List<String> = emptyList(), val id: String = "", val cues: List<SlideStep> = emptyList(), val checkpoint: String = "", val reset: (suspend () -> Unit)? = null, val audienceProvider: (() -> AudienceContent)? = null)

Represents a single step in a slideshow

Declaration Summary
val title: String
val noteHtml: String?
val noteAnchorBox: Box?
val targetBox: Box?
val targetProperty: String
val targetLine: Int?
val highlightLine: Int?
val persistent: Boolean
val animated: Boolean
val withChat: Boolean
val customChatId: String?
val annotations: List<SlideAnnotation>
val onEnter: (suspend () -> Unit)?
val onExit: (suspend () -> Unit)?
val linkCallbacks: Map<String, suspend LinkCallbackContext.() -> Unit>
val audience: AudienceContent?
val sectionPath: List<String>
val id: String
val cues: List<SlideStep>
val checkpoint: String
val reset: (suspend () -> Unit)?
val audienceProvider: (() -> AudienceContent)?

SlideMarkdown

object SlideMarkdown

Markdown renderer for slideshow notes with nav shortcuts.

Declaration Summary
fun render(markdown: String): String

SlideShowInstance

class SlideShowInstance(val hostBox: Box, val steps: List<SlideStep>, customId: String? = null, var presentationOptions: PresentationOptions? = null)

A running instance of a slideshow

Declaration Summary
val hostBox: Box
val steps: List<SlideStep>
var presentationOptions: PresentationOptions?
var currentCueIndex: Int
val slideshowId: String
var currentStepIndex
val currentStep: SlideStep?
suspend fun goToStep(index: Int, cueIndex: Int = 0) Goes to a specific step
suspend fun executeCallback(callbackName: String): Boolean Execute a custom link callback by name
suspend fun next() Goes to the next step
suspend fun previous() Goes to the previous step
suspend fun home() Jump back to the first step
suspend fun start() Starts the slideshow from the beginning
suspend fun stop() Stops the slideshow and hides notes
suspend fun command(command: String) Execute a navigation request in order; callbacks may navigate within the same command.
fun hasNext(): Boolean Returns true if there is a next step
fun hasPrevious(): Boolean Returns true if there is a previous step

AudioVisualizer.kt

The recorded-audio waveform player: a zoomable, seekable view of a recording, as one self-contained HTML fragment for a box output.

It shows audio that already exists, rather than a spectrum or a live meter, with the interaction an editor expects (wheel to zoom, drag to pan, click to seek, a playhead that tracks playback) and the browser's own transport underneath.

What it draws is a min/max envelope of the visible window, not the whole file decimated. Where a server can summarise a window (AudioVisualizerSource.waveformUrl — Field Audio's waveform endpoint, for instance) the component asks for one envelope per view, so an hour-long recording costs the same as a bar of it; where there is no server it decodes the clip in the browser and does the same arithmetic locally. Min/max rather than an average is what keeps a single-sample click visible in a window drawn at 1000 points.

Start from Box.displayAudio for a URL, Box.displayAudioSamples for a FloatArray you generated, or Box.displayAudioBytes for an encoded clip you have in memory.

src/jvmMain/kotlin/afield/ui/visualization/AudioVisualizer.kt — read the source

Used in: Visualizers, Displays, and Little Instruments, Multi-instrument transcription with MuScriptor, Render Field music, then analyse it

Top-level functions

Declaration Summary
fun Box.displayAudio(source: AudioVisualizerSource, channel: String = "audio", options: AudioVisualizerOptions = AudioVisualizerOptions()): String Display a URL-backed recording in this Field box.
fun Box.displayAudioSamples(samples: FloatArray, sampleRate: Int, channel: String = "audio", options: AudioVisualizerOptions = AudioVisualizerOptions(), wavBytes: ByteArray? = null, label: String? = null): String Display generated or otherwise in-memory mono samples in this Field box.
fun Box.displayAudioBytes(bytes: ByteArray, mediaType: String = "audio/wav", channel: String = "audio", durationSeconds: Double? = null, label: String? = null, options: AudioVisualizerOptions = AudioVisualizerOptions()): String Display a short browser-decodable WAV, MP3, Ogg, or similar byte buffer in this Field box.

AudioVisualizerSource

data class AudioVisualizerSource(val audioUrl: String, val waveformUrl: String? = null, val inlineWaveformWavBase64: String? = null, val durationSeconds: Double? = null, val label: String? = null)

Where a waveform player gets its audio, and how it gets the envelope it draws.

Declaration Summary
val audioUrl: String What the <audio> element plays: an ordinary URL or a data: URI. Always required.
val waveformUrl: String? A GET endpoint accepting start_seconds, duration_seconds, and points.
val inlineWaveformWavBase64: String? Browser-compatible PCM16 WAV used only for local waveform analysis, without replacing playback audio.
val durationSeconds: Double? Total length in seconds, if it is known up front.
val label: String? A caption above the waveform, HTML-escaped. Null draws no caption.

AudioVisualizerOptions

data class AudioVisualizerOptions(val width: Int = 960, val waveformHeight: Int = 150, val showPlayhead: Boolean = true, val followPlayhead: Boolean = true, val interactive: Boolean = true, val showNativeControls: Boolean = true, val initialStartSeconds: Double = 0.0, val initialDurationSeconds: Double? = null, val initialPositionSeconds: Double? = null, val syncGroup: String? = null, val syncOffsetSeconds: Double = 0.0, val accent: String = "#63c7dc", val normalize: Boolean = false)

How a waveform player looks and what it lets the user do.

Declaration Summary
val width: Int Maximum width in CSS pixels; the component is fluid below it. Must be positive.
val waveformHeight: Int Height of the waveform plot area in CSS pixels. At least 48, and channels share it as lanes.
val showPlayhead: Boolean Draw the moving playhead while the audio plays.
val followPlayhead: Boolean Keep the view scrolled to the playhead as it moves.
val interactive: Boolean Allow wheel-zoom, drag-pan, click-to-seek and the keyboard shortcuts.
val showNativeControls: Boolean Show the browser's own <audio> transport. The element is always present; this only hides it.
val initialStartSeconds: Double Left edge of the initial view, in seconds. Must be non-negative.
val initialDurationSeconds: Double? Width of the initial view in seconds; null shows the whole clip.
val initialPositionSeconds: Double? Where to place the playhead once metadata has loaded, in seconds; null leaves it at zero.
val syncGroup: String? Optional DOM-event group for synchronizing this player with plots and other time views.
val syncOffsetSeconds: Double Maps player-local seconds to the shared timeline: shared = local + offset.
val accent: String CSS colour for the first channel's trace and the focus highlights. The second channel is amber.
val normalize: Boolean Scale a quiet clip's lane to its own peak instead of ±1. A clip peaking at or above 0.5 keeps the ±1 axis; the axis ticks print whichever range is drawn.

AudioVisualizer

object AudioVisualizer

General-purpose waveform player for Field output areas.

Declaration Summary
fun renderHtml(source: AudioVisualizerSource, options: AudioVisualizerOptions = AudioVisualizerOptions()): String The player as one self-contained HTML fragment — markup, styles and script together.
fun renderSamplesHtml(samples: FloatArray, sampleRate: Int, options: AudioVisualizerOptions = AudioVisualizerOptions(), wavBytes: ByteArray? = null, label: String? = null): String A player for samples you have in memory: mono FloatArray in, self-contained player out.
fun renderBytesHtml(bytes: ByteArray, mediaType: String = "audio/wav", durationSeconds: Double? = null, label: String? = null, options: AudioVisualizerOptions = AudioVisualizerOptions()): String Embed a short browser-decodable recording (WAV, MP3, Ogg, and so on) in a self-contained player. Prefer renderHtml with ordinary audio and waveform URLs for long recordings.
fun fieldAudioWaveformUrl(representationId: String, serviceBaseUrl: String = "/field-audio"): String The Field Audio waveform endpoint for one representation id, ready for AudioVisualizerSource.waveformUrl.

DataFrameCharts.kt

A typed frame, charting its own columns.

DataFrameVisualizer already teaches a frame to render as a Table for .h and to stream its rows for .h2, which is the right compact form: a frame is rows and columns first. What it had no answer for is the other question a reader has about a numeric column, which is what shape is it.

This is that answer, and it is thin — nine lines of column selection over Chart. The interesting decision is which columns, and the rule is the one that makes frame.chart() with no arguments do the obvious thing:

  • a numeric x column and the rest numeric: lines against it, one series per column;
  • a categorical first column (strings, or anything not numeric) and one numeric column: bars, with the category names on the axis — which is what a groupBy(...).count() looks like, and the commonest frame anyone actually plots;
  • no x column named and none obvious: the row index.

Non-numeric cells inside a numeric column become gaps rather than zeroes. A missing measurement is not a measurement of zero, and drawing it as one is the single most common way a plot lies.

src/jvmMain/kotlin/afield/ui/visualization/DataFrameCharts.kt — read the source

Top-level functions

Declaration Summary
fun DataFrame<*>.chart(x: String? = null, y: List<String>? = null, title: String? = null, points: Boolean = true): Chart A Chart of this frame's numeric columns.
fun Box.showChart(frame: DataFrame<*>, x: String? = null, y: List<String>? = null, title: String? = null, channel: String = "output"): Chart A DataFrame charted straight into an output channel — the plot counterpart of letting a frame be a box's last expression, which gives you the table.

DataFrameSchemas.kt

src/jvmMain/kotlin/afield/ui/visualization/DataFrameSchemas.kt — read the source

Top-level functions

Declaration Summary
fun <T> DataFrame<T>.typed(name: String): DataFrame<T> Makes a DataFrame's columns nameable in later executions of the same box — the part the compiler plugin cannot do on its own.

DataFrameVisualizer.kt

Makes Kotlin DataFrame values render as Field values (.h, .h2, display and box return values) without DataFrame having to know Field exists.

DataFrame is a third-party interface, so it cannot implement ToHTML the way Table and Img do. Both registries exist for exactly this: ValuePrinterRegistry for the compact .h form, and H2ValueRegistry for .h2. Everything here is a thin adapter onto primitives that already exist, so a frame gets the same styling, the same expander and the same paging as every other value.

The compact .h form

table already renders a column-oriented table in Field's output style, so a frame's compact form is that table over the first HEAD_ROWS rows, plus a line giving the shape. Only the head is touched: each column is indexed for its first HEAD_ROWS cells rather than the frame being materialised, so printing a million-row frame costs about what printing ten rows costs.

The .h2 scroller

H2ValueSnapshot adapts one value into another for the lazy renderer rather than producing HTML. So .h2 on a frame adapts it into a private list-of-rows view that reads a row only when asked, and the h2 explorer (h2explorer.kt, reached as an iframe onto /h2-explorer?root=<token> and fed over the /ws __h2_explorer__ channel) streams windows of it by index. The server holds the frame rather than a rendering of it behind an .h2 token, and each scroll materialises only the window being looked at, which is what makes a large frame a scroller instead of a large HTML dump. ValueVisualizer's /lazy-value-fragment renderer is a separate path, for expanding a value in place rather than scrolling one.

Going through the registries rather than a bespoke widget also means a frame nested inside another value, a list of frames or a data class with a frame field, expands with the same machinery.

Where the adapters get registered

install is idempotent and runs during server start-up. A bare jvm-main harness does not run that, so tests call it explicitly; DataFrameVisualizerSmokeTest is the worked example.

src/jvmMain/kotlin/afield/ui/visualization/DataFrameVisualizer.kt — read the source

DataFrameVisualizer

object DataFrameVisualizer

The registration point, and the four renderers behind it: a frame, a single column, a single row and a grouped frame, each as one HTML fragment.

Declaration Summary
const val HEAD_ROWS: Int Rows shown in the compact .h form. .h2 streams the whole frame instead.
fun install() Register every DataFrame renderer. Idempotent, and safe to call from any thread.

DataFrameVisualizerSmokeTest.kt

Checks that DataFrame values render through Field's value registries — DataFrameVisualizer.

The assertions are deliberately about dispatch and laziness rather than exact markup, which would make this a change-detector on the table renderer:

  • the registry is reached at all (a frame must not fall through to toString());
  • .h shows a head and says how much it is hiding, without materialising the frame;
  • .h on a large frame produces BOUNDED output — it must not contain the last row's data. That is the observable form of "only the head was rendered". An earlier version of this test counted lambda invocations on an added column instead and passed with a count of zero, because add materialises its column eagerly and cell access never re-invokes the lambda: the assertion was vacuous. Output content is the property worth pinning, since it is what a box actually shows.

install() is called explicitly because Server.main — which calls it in the app — does not run in a bare jvm-main harness.

src/jvmMain/kotlin/afield/ui/visualization/DataFrameVisualizerSmokeTest.kt — read the source

DataFrameVisualizerSmokeTest

object DataFrameVisualizerSmokeTest

Declaration Summary
fun runTests(): Boolean
fun main(args: Array<String>)

FLineEditableSvg.kt

src/jvmMain/kotlin/afield/ui/visualization/FLineEditableSvg.kt — read the source

GeometryFinding.kt

GeometryFinding, the record a geometry checker or agent writes about a piece of geometry for review; it never changes whether a build is accepted.

src/jvmMain/kotlin/afield/ui/visualization/GeometryFinding.kt — read the source

GeometryFinding

data class GeometryFinding(val key: String, val kind: String, val stage: String, val runId: String, val producer: String, val consequence: String, val prompt: String)

Review metadata shared by checkers and agents. A finding never changes build acceptance.

Declaration Summary
val key: String
val kind: String
val stage: String
val runId: String
val producer: String
val consequence: String
val prompt: String
fun json(): JsonObject

H2DomainAdapters.kt

.h2 adapters for the Field types where the reflective default in H2Reflect.kt reads badly.

H2Reflect lifted .h2 from "types someone registered" to "everything", and for a data class or a plain domain object a property tree is exactly right. Four families it is wrong for, each for its own reason:

  • Box — reflection would touch Box.ki, a lazyVar that builds a K2 REPL engine on read. Exploring a box would compile one. This is the concrete case behind the side-effect limit H2Reflect's KDoc admits it cannot detect, and the reason Box is served here.
  • Functions.Fn — its only non-derived field is a private (Double) -> Double. A function's content is its values, so the adapter samples it.
  • FLine — a polyline's content is its node list, and nodes is one property among four.
  • Throwable — a java.* type, refused by the platform-package narrowing, and the value a box produces most often when something is wrong.

Each adapter returns a shape the existing explorer machinery already renders well — a LinkedHashMap gets map paging, and a List<Map<String, Any?>> hits the explorer's RECORD mode and becomes a real table with one column per key. That is deliberate: an adapter that invented its own rendering would have to be maintained in three places.

src/jvmMain/kotlin/afield/ui/visualization/H2DomainAdapters.kt — read the source

Top-level functions

Declaration Summary
fun registerH2DomainAdapters() Registers the four adapters above, for Box, Functions.Fn, FLine and Throwable.

H2Reflect.kt

The reflective half of .h2: what a domain object's children are, computed once and shared by every surface that shows them.

.h2 renders a value three separate times — the Browsable tree behind /tree and /browse, the row/column stream behind the embedded /h2-explorer iframe, and the summary line in the widget itself. This file computes the children all three read, reflectively and for any type. A per-type H2ValueSnapshot adapter is the alternative, and stays the route for a type whose getters this cannot walk safely; without either, a non-collection falls through LazyValueVisualizer.visualize's opening guard to ValueVisualizer and .h2 renders as .h.

properties is the single enumerator, so /tree and the iframe cannot disagree about how many fields a value has.

Getters are opaque

A KProperty1 getter is opaque: there is no way to tell a field read from a computed property that opens a file, allocates a GPU buffer or builds a K2 REPL engine. Box.ki is a lazyVar that compiles on read, which is why Box is served by a hand-written adapter in H2DomainAdapters.kt rather than by this file. The two narrowings in readsWellInline (platform packages, and types whose .h rendering is the value) mitigate the general case rather than settling it. If a type's getters are expensive or effectful, give it an adapter.

src/jvmMain/kotlin/afield/ui/visualization/H2Reflect.kt — read the source

ImagingVisualizer.kt

Inline visualizers for the CPU imaging and computer-vision family (afield.stdlib/imaging*.kt): what a gradient, a pyramid, a flow field or a list of corners looks like when a box evaluates one.

The imaging layer is around 250 public symbols of raster and feature code, and Img is the only one of its types that carries a picture of its own. Everything else falls through to a reflective object tree unless something here claims it, and there a list of forty Corners is a wall of {x=…, y=…, response=…} and a GaussianPyramid is GaussianPyramid@3f1a2b. Those are exactly the values you want to look at.

Where this hooks in

Three one-line hooks, small enough that this file can grow without touching ValueVisualizer.kt:

  • Img.toHTML delegates to img. Img implements ToHTML, which ValueVisualizer dispatches before its built-in table, so improving the Img picture has to happen there.
  • ValueVisualizer.visualizeBuiltIn's fallback branch tries format first, which covers the non-data classes (Gradient, GaussianPyramid, LaplacianPyramid, FlowField, ComponentLabelling).
  • ValueVisualizer.formatList tries formatList first, which covers the homogeneous detector outputs (List<Corner>, List<HoughLine>, List<HoughCircle>, List<FlowVector>, List<Img>).

Turning a float image into pixels

The family is float-channel and unbounded: a Sobel dx, a Laplacian and a difference-of-Gaussians are legitimately signed, an unnormalised response legitimately exceeds 1, and a division somewhere upstream legitimately produces a NaN. A visualizer that clamps all of those to 0..1 shows a flat white or a flat black rectangle and tells you nothing, so the raster renderer picks one of three modes:

  • direct — every finite sample already sits in 0..1. Shown as-is; nothing is rescaled.
  • signed ±m — some finite sample is negative. The range is made symmetric about zero (m = max(|min|, |max|)) so that zero is a fixed, recognisable colour. A 1-channel image is additionally run through the coolwarm diverging ramp from Colormaps (blue negative, pale zero, red positive), because grey cannot carry the sign of a gradient. Multi-channel signed data is offset and scaled per sample without a ramp.
  • auto-ranged — everything is non-negative but the maximum exceeds 1. Linearly rescaled, with the data's true range printed beside it, so a bright pixel is never mistaken for 1.0.

The caption names the mode whenever the mode rescaled something; the statistics table inside the disclosure names it in every case, including the fourth one, where no sample is finite at all.

Non-finite samples (NaN, ±∞) are counted, excluded from every statistic, and painted magenta so they are impossible to miss. Excluding them is what keeps one NaN out of the minimum and the maximum, where it would take the whole display range with it and quantise to a uniformly black thumbnail captioned range NaN…NaN.

A 4-channel image is drawn over a checkerboard, so transparent and black are distinguishable.

The disclosure

Each raster visualizer carries a closed <details> with the per-channel strip, a per-channel histogram computed over the full-resolution samples, and the numeric stats. It is a plain <details> element rather than a lazy-value-fragment expander link, so it needs no running server and works identically in a live box, in a docs capture and in a saved HTML file.

src/jvmMain/kotlin/afield/ui/visualization/ImagingVisualizer.kt — read the source

ImagingVisualizer

object ImagingVisualizer

The dispatch surface: format for a single imaging value, formatList for a homogeneous list of them, and img for the Img thumbnail that Img.toHTML calls directly.

Declaration Summary
fun format(value: Any): String? Render one of the imaging family's non-list types, or null if value is not one of them (in which case ValueVisualizer carries on with its reflective/toString fallback).
fun formatList(list: List<*>): String? Render a homogeneous list of imaging values as one picture, or null when the list is not homogeneously one of the supported element types (empty lists and mixed lists fall through to ValueVisualizer's ordinary list rendering).
fun img(image: Img): String The inline picture for an Img: an aspect-correct thumbnail of the actual pixels under the range policy this file's overview sets out, a one-line readout, and a disclosure holding the per-channel strip, histogram and statistics.

LazyValueVisualizer.kt

The lazy half of value display: rather than inline a large structure, show one summary line plus links into the server-side browsers.

LazyValueVisualizer.visualize — reached in a box as h2 — passes anything small or unrecognised straight to ValueVisualizer. For maps, lists, arrays and FloatBuffer / IntBuffer it instead retains the value under a token and emits a summary, outliner and columns links, and an embedded /h2-explorer iframe. Those tokens live in an in-memory retention table (10 minutes, 256 entries), so the links only resolve against the running server and go stale after that.

The private Browsable wrappers behind those routes page rather than copy: children are grouped into range-labelled directories of at most 96 entries, and a map with more than 20,000 entries is iterated on demand instead of snapshotted. registerH2Value adds a per-type adapter returning an H2ValueSnapshot when a type should be summarised or unwrapped differently, and lazyInlineExpanderHtml is the inline "… +N more" link ValueVisualizer emits for elided content.

src/jvmMain/kotlin/afield/ui/visualization/LazyValueVisualizer.kt — read the source

Top-level functions

Declaration Summary
inline fun <reified T : Any> registerH2Value(noinline adapter: (T) -> H2ValueSnapshot) Registers a .h2 adapter for T, so that a value of that type is summarised or unwrapped the way adapter says rather than by the reflective default.

Top-level values

Declaration Summary
val Any?.h2: String The box-facing entry point: someBigThing.h2 is the explorable rendering, where .h is the inline one.

H2ValueSnapshot

data class H2ValueSnapshot(val value: Any?, val summary: String? = null, val title: String? = null)

What a .h2 adapter returns: the value to explore in place of the one it was handed, and the wording to show for it.

Declaration Summary
val value: Any? What the explorer pages through in place of the value the adapter was given.
val summary: String? The one-line description shown above the explorer, used verbatim in place of the generated Type (size=N) line.
val title: String? The label the explored value carries in the browsers.

H2ValueRegistry

object H2ValueRegistry

The table of per-type .h2 adapters, and the rule that decides which one serves a given value.

Declaration Summary
fun <T : Any> register(clazz: Class<T>, adapter: (T) -> H2ValueSnapshot) Records adapter against clazz itself, replacing any adapter already registered for that exact class.

LazyValueVisualizer

object LazyValueVisualizer

The renderer behind .h2, and the only public entry point into it.

Declaration Summary
fun visualize(value: Any?, appendable: Boolean = false): String Renders value as the .h2 panel: a summary line, outliner and columns links into the server-side browsers, and an embedded explorer iframe.

SignalViewer.kt

A zoomable viewer for functions of time: each SignalChannel is a lane drawn as a min/max envelope, resampled as you zoom.

SignalViewer holds the lanes and answers the browser's window queries; Box.displaySignal shows it in an output channel and keeps it alive while it is visible. A lane built with certifiedSignalChannel also draws the interval-arithmetic bound of its function behind the sampled envelope.

src/jvmMain/kotlin/afield/ui/visualization/SignalViewer.kt — read the source

Used in: A function, played and read back, Visualizers, Displays, and Little Instruments

Top-level functions

Declaration Summary
fun certifiedSignalChannel(name: String, fn: Functions.Fn, minimum: Double = 0.0, maximum: Double = 1.0): SignalChannel A lane whose sampled envelope and certified band both come from fn.
fun Box.displaySignal(viewer: SignalViewer, channel: String = "signal", options: SignalViewerOptions = SignalViewerOptions()): String Replace a box output, releasing its previous signal's evaluator references.

SignalChannel

data class SignalChannel(val name: String, val value: (Double) -> Double, val minimum: Double = 0.0, val maximum: Double = 1.0, val comparison: ((Double) -> Double)? = null, val certified: Functions.Fn? = null)

One lane, with an optional earlier version drawn in dashed grey. Evaluators must be pure.

Declaration Summary
val name: String
val value: (Double) -> Double
val minimum: Double
val maximum: Double
val comparison: ((Double) -> Double)?
val certified: Functions.Fn? Opt-in: the same function as an Functions.Fn, whose per-bin certified bound (certifiedEnvelope) is drawn as a band behind the sampled envelope — the bound no feature between samples can escape. Must compute what value computes; certifiedSignalChannel builds both from one Fn. A graph with formula-less ops is refused, and the lane says so.

SignalViewerOptions

data class SignalViewerOptions(val width: Int = 1100, val laneHeight: Int = 130, val label: String = "Signal", val comparisonLabel: String = "previous", val initialStart: Double? = null, val initialDuration: Double? = null, val syncGroup: String = "", val viewKey: String = "")

Declaration Summary
val width: Int
val laneHeight: Int
val label: String
val comparisonLabel: String
val initialStart: Double?
val initialDuration: Double?
val syncGroup: String Same DOM-event timeline used by AudioVisualizer. Units are seconds.
val viewKey: String Stable across revisions to keep the user's view when the output is replaced.

SignalTrace

data class SignalTrace(val low: List<Double>, val high: List<Double>)

SignalLaneWindow

data class SignalLaneWindow(val name: String, val minimum: Double, val maximum: Double, val current: SignalTrace, val comparison: SignalTrace?, val certified: SignalTrace? = null, val certifiedRefusal: String? = null)

Declaration Summary
val current: SignalTrace, val comparison: SignalTrace?
val certified: SignalTrace? Per-bin certified bounds over the same bins as current (one per sample interval in waveform mode).

SignalWindow

data class SignalWindow(val start: Double, val end: Double, val mode: String, val sampleSpacing: Double, val lanes: List<SignalLaneWindow>)

Declaration Summary
val sampleSpacing: Double, val lanes: List<SignalLaneWindow>)
fun toJson(): String The wire form: JSON with every non-finite value (an empty or gap bin) written as null.

SignalViewer

class SignalViewer(channels: List<SignalChannel>, val start: Double, val end: Double, val sampleStep: Double = 0.02, val gaps: Boolean = false) : AutoCloseable

A range-query source for the frontend signal viewer. Like AudioVisualizer, it sends only the visible envelope; unlike recorded audio, functions are evaluated more finely as you zoom in. Envelopes preserve extrema on the sampling grid, not arbitrarily narrow features between samples. Keep this object alive while its HTML is visible, or use Box.displaySignal, which does so for you.

Declaration Summary
val start: Double
val end: Double
val sampleStep: Double Largest requested sampling interval. A query is capped at 500,000 intervals per lane.
val gaps: Boolean Treat non-finite values as gaps instead of refusing the query. A bin with no finite value is sent as null, which the viewer draws as a break; Fn.plot uses this for functions with holes.
val channels
val id: String
override fun close()
fun window(from: Double, to: Double, points: Int, checkActive: () -> Unit = {}): SignalWindow Bounded response size; checkActive permits a server request to cancel between sample batches.
fun renderHtml(options: SignalViewerOptions = SignalViewerOptions()): String

TimeViewer.kt

The HTML shell of Field's one time-axis viewer, shared by SignalViewer, Fn.plot, AudioVisualizer and the Audio AI analysis views. It writes the chrome (title, status chip, zoom/fit buttons, a rounded plot frame, a readout line and an overview strip) plus a JSON config, and inlines signal-viewer.js, which draws with Field's bundled Plotly.

Callers choose a source in render's config: range (a /signal-viewer/{id} query per visible window), embedded (a static window in the page), audio (an <audio> element and its envelope) or analysis (a Field Audio preview). Everything else is shared: the view window, the time bus (field-time-window, -cursor, -seek, -transport), viewKey persistence and the DOM-overlay playhead. See dev_docs/SIGNAL_VIEWER.md.

src/jvmMain/kotlin/afield/ui/visualization/TimeViewer.kt — read the source

ValueVisualizer.kt

The default value display for Field output areas: a Kotlin value in, a compact HTML fragment out.

ValueVisualizer.visualize is the entry point; Any.h and Box.viz are how it is usually reached — .h hands back the fragment, viz displays it into a channel of a live box. A type can take over its own rendering either by implementing ToHTML or by registering a printer with registerValuePrinter; both are trusted to return well-formed HTML, while ordinary strings, characters, keys and reflected values are escaped here.

Output is bounded — ValueVisualizer's own KDoc below covers dispatch order, the built-in formatters and the size limits. Whatever those limits elide (the tail of a long string or list, extra fields of an object) becomes an expander link, resolved later by renderLazyFragment from the server's /lazy-value-fragment route, so the rest of the value is only ever rendered if someone asks for it.

src/jvmMain/kotlin/afield/ui/visualization/ValueVisualizer.kt — read the source

Used in: Agent transcript gallery, DataFrames in a Box, Visualizers, Displays, and Little Instruments

Top-level functions

Declaration Summary
fun Box.viz(channel: String = "viz", value: Any?) Extension function to visualize a value in a Box's display output
inline fun <reified T : Any> registerValuePrinter(noinline printer: (T) -> String) Register a custom printer for a type
fun jsonLeafValue(p: kotlinx.serialization.json.JsonPrimitive): Any? The Kotlin value a kotlinx JSON leaf holds: null, a Boolean, a Long or Double, or the string's content without its quotes. What .h and .h2 show in place of the JsonPrimitive itself.

Top-level values

Declaration Summary
val Any?.h : String

ToHTML

interface ToHTML

Interface for objects that can provide their own HTML representation

Declaration Summary
fun toHTML(): String

ValuePrinterRegistry

object ValuePrinterRegistry

Registry for custom value printers

Declaration Summary
fun <T : Any> register(clazz: Class<T>, printer: (T) -> String) Register a custom printer for a specific type

ValueVisualizer

object ValueVisualizer

Render a Kotlin value as a compact, escaped HTML fragment for Field output areas.

Declaration Summary
fun visualize(value: Any?, appendable: Boolean = false): String Visualize any value and return compact HTML representation

charts.kt

Plots — the structure, the scales, and the one renderer for them.

Chart is to a plot what Table is to a grid: build it, and it arrives on screen in the repo's style with no markup in the way. It implements ToHTML, so display(channel, chart), chart.h and a box whose last expression is a Chart all render through Chart.toHTML; Box.showChart is the terse form.

This file or plotly.kt

plotly.kt — showSignals, linePlot, scatter, violin — is the default, and better at being a chart: real text rather than glyph geometry, a plot that reflows to its panel instead of being fitted and centred, and log, subplot and box axes this file does not carry. Reach for a Chart for the three things Plotly cannot do: composing a plot with other FLine geometry in one drawing under a shared transform, picking, and Chart.expect.

Axis labels here are real glyph geometry (labelLines), filled and with their counters intact, because showPlaneGrid has nowhere to put text. niceTicks is Heckbert's nice-number tick algorithm in public. dev_docs/chart_primitive.md holds the Plotly comparison capture and the survey of what each medium in the repo was drawing for itself.

Marks pick themselves up

Box.lines switches to the interactive SVG renderer the moment any line carries FLine.onClick or FLine.hoverColor (someSkiko.kt). Point and bar marks set both, so hovering a bar and clicking a datum need no configuration — see Chart.pickable for the escape hatch and the cost.

Chart.expect

Exactly as in Table. A plot in a doc or a live box usually carries a claim — "no residual exceeds 1e-6", "every voice stayed inside the guard band". State the claim and it is drawn as a band, the data outside it recolours, the footnote says how many points broke it, and Chart.deviations hands the same list to a test.

src/jvmMain/kotlin/afield/ui/visualization/charts.kt — read the source

Used in: Charts as FLines, Filling a solid: tetrahedral meshing with TetGen

Top-level functions

Declaration Summary
fun niceNumber(x: Double, round: Boolean): Double Heckbert's "nice numbers": round x to one of 1, 2, 5 or 10 times a power of ten.
fun niceTicks(lo: Double, hi: Double, target: Int = 6): List<Double> Round tick values covering [lo, hi], about target of them — the one copy of the algorithm that five files had each written privately.
fun logTicks(lo: Double, hi: Double, maxTicks: Int = 12): List<Double> Decade ticks covering [lo, hi], with 1/2/5 mantissas while the span is small enough to fit.
fun tickLabel(value: Double, step: Double = 0.0): String A tick's label: just enough decimals for step, trailing zeros trimmed, and never -0.
fun tickLabels(values: List<Double>, step: Double = 0.0): List<String> The labels for a whole axis, in one notation.
fun hexColor(hex: String): Vec4 #rrggbb or #rrggbbaa as a Vec4 in 0..1.
fun chart(vararg series: Pair<String, List<Number>>, x: List<Number>? = null, title: String? = null, xLabel: String? = null, yLabel: String? = null, note: String? = null): Chart A Chart of named series against a shared x — shaped like table and showSignals, so the same data can be a grid or a plot by changing one word.
fun Box.showChart(chart: Chart, channel: String = "output"): Chart Put a Chart on the screen and hand it back — the plot counterpart of Box.showTable.
fun Box.showChart(vararg series: Pair<String, List<Number>>, channel: String = "output", x: List<Number>? = null, title: String? = null, xLabel: String? = null, yLabel: String? = null, note: String? = null): Chart Builds a Chart from named series with chart and displays it.

Top-level values

Declaration Summary
val CHART_MUTED: Vec4 Muted grey for axes, ticks and notes — the plot counterpart of TABLE_MUTED.
val CHART_GRID: Vec4 The gridline colour: the same grey at a tenth of the weight, so data always wins.
val CHART_BAD: Vec4 What a failed Chart.expect recolours to, matching CellRole.BAD's intent.

ScaleKind

enum class ScaleKind

Which arithmetic a Scale uses between data space and pixel space.

Constants: LINEAR, LOG.

Scale

class Scale(val lo: Double, val hi: Double, val pixelLo: Double, val pixelHi: Double, val kind: ScaleKind = ScaleKind.LINEAR)

One axis: a data interval, a pixel interval, and the arithmetic between them.

Declaration Summary
val lo: Double The low end of the data interval.
val hi: Double The high end of the data interval.
val pixelLo: Double The pixel lo maps to.
val pixelHi: Double The pixel hi maps to. May be less than pixelLo; see the class note on y.
val kind: ScaleKind Linear by default.
fun toPixel(value: Double): Double Where value lands, in pixels. NaN when the value has no place on this scale.
fun toData(pixel: Double): Double The inverse of toPixel — what data value a pixel names. The thing a click needs.
fun ticks(target: Int = 6): List<Double> Round tick values covering the domain: niceTicks when linear, logTicks when log.
fun tickStep(target: Int = 6): Double How far apart ticks came out, for tickLabel's benefit. Zero when there is one tick.
override fun toString(): String

MarkKind

enum class MarkKind

What a ChartMark draws.

Constants: POINTS, LINE, AREA, BARS, HISTOGRAM, HRULE, VRULE, BAND.

ChartMark

class ChartMark internal constructor(val kind: MarkKind, val label: String, val x: DoubleArray, val y: DoubleArray, val baseline: DoubleArray?, val binWidth: Double, var color: Vec4, var size: Double, var thickness: Double)

One layer of a Chart: what to draw, the numbers to draw it from, and how it looks.

Declaration Summary
val kind: MarkKind What this mark draws.
val label: String The series name: the legend entry, and the head of a datum's hover label.
val x: DoubleArray Data-space x per datum. Integer positions 0 until n for MarkKind.BARS.
val y: DoubleArray Data-space y per datum. For MarkKind.BAND and MarkKind.HRULE the upper edge.
val baseline: DoubleArray? The lower edge, for MarkKind.AREA, MarkKind.BARS, MarkKind.HISTOGRAM and MarkKind.BAND.
val binWidth: Double Bar/bin width in data units; 0 means "derive it from the spacing".
var color: Vec4 Colour, RGBA in 0..1. Defaults to the next unused slot in signalPalette — next unused, so passing a colour explicitly leaves the slot for the following mark rather than burning it.
var size: Double Dot radius, in pixels.
var thickness: Double Stroke width, in pixels.
val count: Int How many data points this mark has.
val isPickable: Boolean Whether a reader can hover and click individual data of this mark.
override fun toString(): String

ChartPick

data class ChartPick(val series: String, val index: Int, val x: Double, val y: Double, val category: String?, val hit: FLineIntersection)

One datum of one mark, as reported to Chart.onPick when a reader clicks it.

Declaration Summary
val series: String The ChartMark.label of the series clicked.
val index: Int Which datum of that series, by index.
val x: Double Its data-space x. For a bar, its slot.
val y: Double Its data-space y.
val category: String? The category name, for a bar; null otherwise.
val hit: FLineIntersection What the interactive SVG renderer reported about the hit itself.

ChartDeviation

data class ChartDeviation(val series: String, val index: Int, val x: Double, val y: Double, val because: String?)

One datum that failed a Chart.expect check: which series and index, its value, and the reason.

Declaration Summary
val series: String
val index: Int
val x: Double
val y: Double
val because: String?

Chart

class Chart(var title: String? = null, var xLabel: String? = null, var yLabel: String? = null, var note: String? = null) : ToHTML

Marks, axes, and the claims made about them.

Declaration Summary
var title: String? Drawn above the plot in the first palette colour.
var xLabel: String? Drawn under the x axis, centred.
var yLabel: String? Drawn to the left of the y axis, rotated a quarter turn.
var note: String? A footnote under the figure. Deviation counts are appended to it, as in Table.note.
var width: Double Figure width in pixels.
var height: Double Figure height in pixels. See width on why the default aspect is panel-shaped.
var xKind: ScaleKind The x axis's arithmetic.
var yKind: ScaleKind The y axis's arithmetic.
var xDomain: Pair<Double, Double>? An explicit x domain, or null to take it from the data.
var yDomain: Pair<Double, Double>? An explicit y domain, or null to take it from the data.
var xTickTarget: Int About how many x ticks to aim for.
var yTickTarget: Int About how many y ticks to aim for.
var legend: Boolean Whether to draw the series legend. Off automatically when there is one unnamed series.
var grid: Boolean Whether to draw gridlines at the ticks.
var clip: Boolean Whether the figure is kept inside the frame it declared. On, and there is a published figure behind it.
var pickable: Boolean Whether point, bar and bin marks are individually hoverable and clickable.
var pickLimit: Int Above this many data in one mark, that mark stops being individually pickable and is drawn as a single line — the dots are still there, they just share one path.
var maxPoints: Int How many data of one mark are drawn before the rest are elided, and the footnote says so — the plot counterpart of Table.maxRows.
var onPick: ((ChartPick) -> Unit)? What a click on a datum calls. null means the datum's own label is printed, which is the behaviour that makes a fresh chart worth clicking before anyone has written a handler.
val marks: List<ChartMark> The layers added so far, in draw order.
val xCategories: List<String>? The category names on the x axis, when a bars mark put them there.
fun points(label: String, x: List<Number>, y: List<Number>, color: Vec4? = null, size: Double = 3.0): Chart A dot per datum. Pickable, so this is what makes a chart clickable.
fun points(label: String, y: List<Number>, color: Vec4? = null, size: Double = 3.0): Chart points against the sample index.
fun line(label: String, x: List<Number>, y: List<Number>, color: Vec4? = null, thickness: Double = 1.6): Chart A polyline through the data in the order given.
fun line(label: String, y: List<Number>, color: Vec4? = null, thickness: Double = 1.6): Chart line against the sample index — the shape showSignals draws.
fun area(label: String, x: List<Number>, y: List<Number>, base: Number = 0.0, color: Vec4? = null): Chart A line with the region down to base filled at a fifth of the alpha.
fun bars(label: String, categories: List<String>, values: List<Number>, color: Vec4? = null, base: Number = 0.0): Chart One bar per category, which puts the category names on the x axis.
fun bars(label: String, vararg values: Pair<String, Number>, color: Vec4? = null): Chart bars written inline: bars("cost", "gates" to 12, "depth" to 4).
fun histogram(label: String, values: List<Number>, bins: Int = 0, color: Vec4? = null): Chart values binned and drawn as bars on a numeric x axis.
fun rule(y: Number, label: String = "", color: Vec4 = CHART_MUTED): Chart A horizontal reference line at y, across the whole plot.
fun vrule(x: Number, label: String = "", color: Vec4 = CHART_MUTED): Chart A vertical reference line at x, down the whole plot.
fun band(lo: Number, hi: Number, label: String = "", color: Vec4 = CHART_GRID): Chart A shaded horizontal band between lo and hi.
fun expect(min: Double = Double.NEGATIVE_INFINITY, max: Double = Double.POSITIVE_INFINITY, series: String? = null, because: String? = null): Chart States that every datum's y lies in [min, max].
fun expect(series: String? = null, because: String? = null, ok: (x: Double, y: Double) -> Boolean): Chart States a claim about every datum, given its x and y — the general form, with no band to draw.
val deviations: List<ChartDeviation> Every datum that failed an expect check — the same list the drawing recolours, so a test can assert on the claim the figure makes rather than on its geometry.
fun resolvedXDomain(): Pair<Double, Double> The x domain that will actually be drawn: xDomain if set, else derived from the data.
fun resolvedYDomain(): Pair<Double, Double> The y domain that will actually be drawn: yDomain if set, else derived from the data.
fun toFLines(): List<FLine> The whole figure, in pixels, as lines — the one output everything else is built on.
override fun toHTML(): String The same HTML Box.lines would display for toFLines — how .h and display work.
fun toText(): String Title, both domains with their kinds, and one line per mark, for a log or a failure message.
override fun toString(): String
fun plotRect(): Rect The plot area, in figure pixels — the rectangle the axes bound and mark geometry is clipped to.

flipbox.kt

A checkbox drawn in the editor that re-runs one block of a box without re-running the box.

flipbox extends Marker, so it is reached through a marker uid in the source (__m("aVq3x-k2").flipbox(true) { … }), and the checkbox is drawn at that call: over a bare true or false argument where there is one, and before the call otherwise.

The rest of the file is that decoration. A RawDecoration carries the glyph, its hover colour and a click callback, and is rewritten after every evaluation and after every click, so the editor always shows the current state rather than the argument's.

It belongs with the other marker decorations in decorations.kt (toggle, beep, bop, liveNumber), and is the one that owns a block rather than a value.

src/jvmMain/kotlin/afield/ui/visualization/flipbox.kt — read the source

Top-level functions

Declaration Summary
fun Marker.flipbox(initial: Boolean = false, block: () -> Unit): Boolean A block with a checkbox drawn in the editor over its argument: it runs when the box evaluates with the box ticked, and you tick it to run the block again. Each evaluation unticks it.

heatmap.kt

A rectangular array of numbers as a colour-mapped picture in a box output, in one call.

The file is two layers. heatmapImage is pure: a rectangular matrix in, a RasterImage of ARGB pixels out, with the colour scale, the optional log transform, the bilinear upsample and the overlay trace all resolved there, so a test or a docs capture can render one without a Box. Box.showHeatmap adds the two steps a box wants, encoding to PNG and displaying it into an output channel, and returns the bytes so the caller can save or re-display them. Box.showImage is that same second layer over a pixel buffer you already have.

Two conventions decide which way up the picture comes out. The matrix is read row-major, outer index the row and inner index the column, so rows are the vertical axis; and flipY defaults to true, drawing row 0 at the bottom, which is what a spectrogram's frequency bins want. A spectrogram stored the other way round, frame-outer and bin-inner, is the transpose = true case: frames become x and bins become y.

Colour is a Colormaps name and a range. Under normalize the range is the data's own minimum and maximum; vmin and vmax pin it instead, which is what makes two captures comparable; logScale runs each value through log10(max(v, 1e-12)) first, for magnitude spectra and anything else spanning orders of magnitude.

src/jvmMain/kotlin/afield/ui/visualization/heatmap.kt — read the source

Top-level functions

Declaration Summary
fun heatmapImage(matrix: Array<DoubleArray>, colormap: String = "viridis", width: Int = 1200, height: Int = -1, normalize: Boolean = true, vmin: Double = Double.NaN, vmax: Double = Double.NaN, logScale: Boolean = false, smooth: Boolean = true, flipY: Boolean = true, transpose: Boolean = false, overlay: List<Pair<Double, Double>>? = null, overlayColor: Int = -0x1, overlayThickness: Int = 2): RasterImage Render a 2-D matrix to a colour-mapped RasterImage — the pure, Box-free core of showHeatmap (usable from tests, docs harnesses, or any non-Box context).
fun Box.showHeatmap(matrix: Array<DoubleArray>, colormap: String = "viridis", channel: String = "output", width: Int = 1200, height: Int = -1, normalize: Boolean = true, vmin: Double = Double.NaN, vmax: Double = Double.NaN, logScale: Boolean = false, smooth: Boolean = true, flipY: Boolean = true, transpose: Boolean = false, overlay: List<Pair<Double, Double>>? = null, overlayColor: Int = -0x1, overlayThickness: Int = 2): ByteArray Terse "put this 2-D field on the screen as a colour-mapped heatmap" helper — the raster counterpart of showSignals.
fun Box.showHeatmap(matrix: List<List<Number>>, colormap: String = "viridis", channel: String = "output", width: Int = 1200, height: Int = -1, normalize: Boolean = true, vmin: Double = Double.NaN, vmax: Double = Double.NaN, logScale: Boolean = false, smooth: Boolean = true, flipY: Boolean = true, transpose: Boolean = false, overlay: List<Pair<Double, Double>>? = null, overlayColor: Int = -0x1, overlayThickness: Int = 2): ByteArray List<List<Number>> convenience — copies into a rectangular Array<DoubleArray>.
fun Box.showImage(pixels: IntArray, width: Int, height: Int, channel: String = "output"): ByteArray Show a ready-made ARGB pixels raster as an inline image (encode + display), returning the PNG bytes. The thin sibling of showHeatmap for when you already have a pixel buffer.

RasterImage

class RasterImage(val pixels: IntArray, val width: Int, val height: Int)

A colour-mapped raster produced by heatmapImage: ARGB pixels laid out row-major, width × height. Call png for PNG bytes ready to display or persist.

Declaration Summary
fun png(): ByteArray Encode to PNG bytes (ARGB, sRGB) via PngEncoder.

intervalPicture.kt

Pictures of what interval arithmetic proved about a function: Functions.Fn.intervalPicture draws the search enclosures and certified bounds behind a sampled curve, and Box.showIntervals displays one in a box's output.

src/jvmMain/kotlin/afield/ui/visualization/intervalPicture.kt — read the source

Used in: Proving a Function's Range

Top-level functions

Declaration Summary
fun Functions.Fn.intervalPicture(a: Double, b: Double, tolerance: Double = 1e-6, bins: Int = 240, title: String? = null, maxEnclosures: Int = 50_000): FnIntervalPicture Proves this function's range on [a, b] and draws the proof — see FnIntervalPicture.
fun Box.showIntervals(fn: Functions.Fn, a: Double, b: Double, channel: String = "intervals", tolerance: Double = 1e-6, title: String? = null): FnIntervalPicture Draws fn's intervalPicture into channel and returns it.

FnIntervalPicture

class FnIntervalPicture(val fn: Functions.Fn, val a: Double, val b: Double, val range: FnCertifiedRange, val envelope: FnCertifiedEnvelope, val pieces: List<FnSearchPiece>, val samples: Int = 1200, val title: String? = null) : ToHTML

A picture of what interval arithmetic proved about a function, and where it had to work to prove it.

Declaration Summary
val fn: Functions.Fn
val a: Double
val b: Double
val range: FnCertifiedRange
val envelope: FnCertifiedEnvelope
val pieces: List<FnSearchPiece>
val samples: Int
val title: String?
var width: Double
var height: Double
var maxBoxes: Int At most this many search boxes are drawn — the first ones, which are the widest.
override fun toHTML(): String
fun svg(): String

plotly.kt

Plotly helpers — the short way to get a chart out of a box, wrapping space.kscience.plotly.

linePlot, scatter, scatterSized and violin cover the common shapes (lists of numbers, Vec2, Vec3) and hand back a Plot already run through darkMode, the repo dark theme with data-first margins. showSignals goes one step further: pass "label" to values pairs and it traces them against a shared x with the signalPalette, optionally min-max normalized and with dashed reference marks, then displays the plot into a channel of a live Box — it is already on screen when it returns, so do not display it again.

toHtml renders a Plot into an HTML fragment via PlotlyIntegration. That fragment calls Plotly.react in the browser, so the page has to have the Plotly library loaded already; without it the fragment logs to the console and draws nothing.

src/jvmMain/kotlin/afield/ui/visualization/plotly.kt — read the source

Used in: Subharmonic Synthesis: an Octave Below, Looking at Data, Values over time: interpolants & keyframes

Top-level functions

Declaration Summary
fun linePlot(vararg data: List<Number>): Plot A dark-mode line plot of one or more lists of numbers against their own sample index — the fastest way to look at a signal. Each list becomes its own hairline trace, so lists of different lengths are fine: they simply run out at different x.
fun linePlot(vararg data: List<Vec2>): Plot A dark-mode line plot of one or more Vec2 polylines, using each point's own x and y — the variant to reach for when x is a real abscissa (time, frequency, arc length) rather than a sample index. Points are joined in list order, so this draws a path and will happily double back on itself.
fun scatter(vararg data: List<Vec2>): Plot A dark-mode markers-only scatter of one or more Vec2 lists, at a fixed 3px marker — for point clouds where the order of the points means nothing and a connecting line would lie about it.
fun scatterSized(vararg data: List<Vec3>): Plot A dark-mode scatter of Vec3 lists in which x and y place the marker and z is the marker size — a bubble chart, for carrying a third quantity per point.
fun violin(vararg data: List<Number>): Plot A dark-mode violin plot, one violin per list of numbers, for comparing whole distributions rather than sequences — order within a list is irrelevant here, only the spread of values.
fun violin(vararg groups: Pair<String, List<Number>>, references: List<Number> = emptyList(), referenceColor: String = "#ffe066", yRange: ClosedFloatingPointRange<Double>? = null, yTitle: String? = null, spanQuantiles: ClosedFloatingPointRange<Double>? = 0.01..0.99, outlierOpacity: Double = 0.25): Plot Named violins side by side, each with an optional reference value drawn as a dashed line across its own violin — for "what was asked for against what came out" over several groups: a target radius against the measured radii, a set point against the readings.
fun Box.showSignals(vararg series: Pair<String, List<Number>>, x: List<Number>? = null, channel: String = "output", title: String? = null, xTitle: String? = null, yTitle: String? = null, marks: List<Pair<String, Number>> = emptyList(), markColor: String = "#ffe066", normalize: Boolean = false, regions: List<SignalRegion> = emptyList(), width: Int? = null, height: Int? = null, floatingLegend: Boolean = false): Plot Put one or more named signals on the screen as a dark-mode Plotly line plot, and return the Plot.
fun Box.showSpectrogram(audio: FloatArray, sampleRate: Int = 48_000, maxHz: Double = 600.0, fftSize: Int = 8192, hopSize: Int = 1024, rangeDb: Double = 70.0, channel: String = "output", title: String? = null): Plot Put a spectrogram of audio on the screen as a Plotly heatmap with time and frequency axes, and return the Plot.
fun Plot.darkMode(): Plot Restyle a Plot into the repo's dark theme and hand back the same instance — near-black paper and plot backgrounds, muted grid and tick colours, tick labels held horizontal, a vertical legend just outside the plot's right edge, and data-first margins that reclaim most of the ~100px Plotly reserves up top for a title.
fun Plot.toHtml(): String Render a Plot to an inline HTML fragment: a plain wrapper div around the plot's own div, which carries a fresh random id and a script that calls Plotly.react on it. Not an iframe and not a whole document — this is exactly what a box output does with a Plot, so the result drops straight into the editor's output pane.

Top-level values

Declaration Summary
val signalPalette: List<String> Cyan/orange-led dark-mode palette for signal traces (leads cyan, then orange).

SignalRegion

data class SignalRegion(val label: String, val from: Number, val to: Number, val color: String = "#ffe066")

A labelled x-span for showSignals' regions: a translucent band from from to to.

PlotlyIntegration

public class PlotlyIntegration : PlotlyRenderer

The PlotlyRenderer behind toHtml: it emits a bare div plus a Plotly.react call against it, rather than the self-contained page plotly-kt's own renderers produce. That is what makes the output embeddable in a box output or a docs page that already loads Plotly.

Declaration Summary
override fun FlowContent.renderPlot(plot: Plot, plotId: String, config: PlotlyConfig): Plot Emit plot into the surrounding kotlinx.html flow as a div with id plotId, followed by an inline script handing the trace data, layout and config to Plotly.react. Returns the plot unchanged.
fun renderFragment(fragment: PlotlyFragment): String Render a whole PlotlyFragment — a piece of kotlinx.html that may hold several plots and ordinary markup between them — to an HTML string, with every plot inside it going through this renderer. The result is wrapped in a single enclosing div; the same "Plotly must already be loaded" caveat applies as for toHtml.

someSkiko.kt

Drawing FLines into a box's output as SVG: Box.lines and Box.l display them, linesToHTML and drawToSVG return the markup, and outputTo draws one line onto a Skia Canvas.

Lines carrying FLine.onClick or FLine.hoverColor are drawn as interactive SVG with click callbacks; everything else goes through Skia's SVG canvas.

src/jvmMain/kotlin/afield/ui/visualization/someSkiko.kt — read the source

Top-level functions

Declaration Summary
fun linesToHTML(lines: List<FLine>, w: Double = -1.0, h: Double = -1.0, fitToSize: Boolean = false): String The exact HTML a non-editable Box.lines would display, without displaying it.
fun Box.lines(vararg m: FLine, channel: String = "lines", w: Double = -1.0, h: Double = -1.0, fitToSize: Boolean = false, editable: Boolean = false, drawing: Boolean = false, liveDrawing: Boolean = false, onEditCommitted: ((List<FLine>) -> Unit)? = null): EditableFLineViewState? Render one or more FLines to SVG and display them in an output channel. If any FLine has FLine.onClick or FLine.hoverColor set, an interactive SVG is generated instead of using the Skia canvas renderer.
fun Box.lines(m: List<FLine>, channel: String = "lines", w: Double = -1.0, h: Double = -1.0, fitToSize: Boolean = false, editable: Boolean = false, drawing: Boolean = false, liveDrawing: Boolean = false, onEditCommitted: ((List<FLine>) -> Unit)? = null): EditableFLineViewState? Render a list of FLines to SVG and display them in an output channel. If any FLine has FLine.onClick or FLine.hoverColor set, an interactive SVG is generated instead of using the Skia canvas renderer.
fun Box.lines(linesByChannel: Map<String, List<FLine>>, w: Double = -1.0, h: Double = -1.0, fitToSize: Boolean = false, splitByChannel: Boolean = false, channel: String = "lines", editable: Boolean = false, drawing: Boolean = false, liveDrawing: Boolean = false, onEditCommitted: ((List<FLine>) -> Unit)? = null): Map<String, EditableFLineViewState> Render FLines from a map, optionally splitting by channel. If any FLine has FLine.onClick or FLine.hoverColor set, an interactive SVG is generated instead of using the Skia canvas renderer.
fun Box.l(m: Map<String, List<FLine>>, w: Double = 200.0, h: Double = 200.0, fitToSize: Boolean = true) Convenience wrapper for example maps.
fun Box.showFLineGeometryExamples(w: Double = 200.0, h: Double = 200.0, fitToSize: Boolean = true) Showcase: render all FLineGeometryExamples with sensible defaults.
fun drawToSVG(n: List<FLine>, w: Double = -1.0, h: Double = -1.0): String Render a list of FLines into an SVG string.
fun outputTo(f: FLine, s: Canvas, antiAlias: Boolean = false) Draw a single FLine to a Skia Canvas, honoring stroke, fill, and point settings.

tables.kt

Tabular output — the structure, and the one renderer for it.

Table is the counterpart of showSignals for data that is rows and columns rather than a signal: build it, hand it to Box.showTable (or just let a box's last expression be one), and it arrives on screen in the repo's output style. It implements ToHTML, so ValueVisualizer dispatches it ahead of everything else — display(channel, table), table.h, and a returned Table all render through Table.toHTML with no extra wiring.

Why this exists: a box that wants a table has, until now, hand-rolled <table style='border-collapse:collapse…'> with its own font stack, paddings and greys. A dozen files under src/ do exactly that and no two agree, which is a lot of markup standing between a reader and what the box is actually saying.

The part that is not just formatting is Table.expect. A table in a doc or a live box usually carries a claim — "every builder advanced one frame per pump", "no residual exceeds 1e-6" — and a reader is otherwise asked to verify it by scanning cells. State the claim and the cells that break it colour themselves, the footnote says how many did, and Table.deviations hands the same list back to a test. That is the difference between showing a grid and showing an assertion you can watch fail.

Trust model, same as the rest of value display: text produced by default formatting is escaped; a string returned from a Table.format lambda is trusted HTML and inserted as-is.

src/jvmMain/kotlin/afield/ui/visualization/tables.kt — read the source

Used in: Frames and the animation loop, How completions are ranked, DataFrames in a Box

Top-level functions

Declaration Summary
fun table(vararg columns: Pair<String, List<Any?>>): Table A Table from named columns — the column-major entry point, shaped like showSignals: table("pump" to pumps, "calls" to calls). Columns of unequal length are padded with null.
fun facts(pairs: List<Pair<String, Any?>?>): Table A label→value summary, which is the commonest figure shape there is: what a toHTML() writes about its own object, what a probe reports, what sits beside a thumbnail.
fun facts(vararg pairs: Pair<String, Any?>?): Table facts with the pairs written inline.
fun recordTable(items: List<*>): Table? A Table over a list of records — a uniform list of data classes (columns from the primary constructor), of maps (columns from the keys), or of equal-length lists (columns by index).
fun Box.showTable(table: Table, channel: String = "output"): Table Put a Table on the screen and hand it back — the terse "this table, on screen, now" helper, the tabular counterpart of showSignals.
fun Box.showTable(vararg columns: Pair<String, List<Any?>>, channel: String = "output", title: String? = null, note: String? = null): Table Builds a Table from named columns with table and displays it.
fun Box.showTable(items: List<*>, channel: String = "output", title: String? = null, note: String? = null): Table Displays a list of records as a table via recordTable, falling back to a single value column when the list is not uniform enough to have columns.

Top-level values

Declaration Summary
const val TABLE_MUTED: String Muted grey for headers, notes and elisions — the same #888 the rest of value display uses.
const val TABLE_TEXT: String Default body text colour, matching ValueVisualizer's wrapper. See TABLE_MUTED on the hex.

Align

enum class Align

Horizontal alignment of a Table column. Numeric columns default to RIGHT, everything else LEFT.

Constants: LEFT, RIGHT, CENTER.

Deviation

data class Deviation(val row: Int, val column: String, val value: Any?, val because: String?)

One cell that failed a Table.expect check: its row index, column name, value, and the reason given.

CellRole

enum class CellRole(internal val cssClass: String, internal val cssVar: String)

The semantic colour roles, shared with markdown()'s {role:text} spans — same six names, same field-md-* classes, so the same --field-* design token backs both. A cell that means "good" and a markdown span that means "good" are therefore the same colour by construction, and neither spells a hex literal.

Constants: ACCENT, MUTED, DIM, OK, WARN, BAD.

Table

class Table(header: List<String>, var title: String? = null, var note: String? = null) : ToHTML

Rows, columns, and the claims made about them.

Declaration Summary
var title: String? Optional line drawn above the table, in the accent colour.
var note: String? Optional footnote spanning every column. Deviation counts are appended to it.
val header: List<String> The column names, in order.
val rows: List<List<Any?>> The rows appended so far, each padded to header's width.
var maxRows: Int How many rows to draw before eliding the tail.
var showHeader: Boolean Whether to draw the header row. A label→value summary built by facts has no header worth showing — "key" and "value" over two obvious columns is furniture — so that turns this off.
fun row(vararg values: Any?): Table Appends one row. Short rows are padded with null; a row wider than header is an error.
fun row(values: List<Any?>): Table Appends one row from a list. Short rows are padded with null; too-wide rows are an error.
fun rows(values: List<List<Any?>>): Table Appends many rows.
fun align(column: String, align: Align): Table Overrides the inferred alignment of a column.
fun color(column: String, color: String): Table Colours a column's cells.
fun accent(vararg columns: String): Table Colours these columns from signalPalette, in order — so a table and a plot of the same quantities agree on which colour means which series.
fun role(column: String, role: CellRole): Table Gives a column's cells a semantic CellRole instead of a colour.
fun roleBy(column: String, role: (Any?) -> CellRole?): Table Gives each cell of a column a CellRole chosen from its value — the per-cell counterpart of color, for a column that means something different row by row (ran / did not run, converged / diverged). Return null to leave a cell alone.
fun bar(column: String, max: Double? = null, width: Int = 120): Table Draws a numeric column's cells as proportional bars as well as text — the shape a ranked score wants: top-k labels, vote tallies, per-stage timings.
fun format(column: String, format: (Any?) -> String): Table Formats a column's cells. The returned string is trusted HTML and is not escaped, which is how a cell gets a link, a unit, or a nested fragment; escape it yourself if it came from outside.
fun expect(column: String, because: String? = null, ok: (Any?) -> Boolean): Table States a claim about one column: cells for which ok is false take CellRole.BAD, gain a tooltip, are counted in the footnote, and appear in deviations.
fun expect(because: String? = null, ok: (row: Int, column: String, value: Any?) -> Boolean): Table States a claim about every cell, given its row index, column name and value.
val deviations: List<Deviation> Every cell that failed an expect check — the same list the rendering colours, so a test can assert on the claim the figure makes rather than on its markup.
fun toText(): String A plain-text rendering, for a terminal, a log, or a test's failure message.
override fun toHTML(): String The rendered table — dispatched by ValueVisualizer for display, .h and box return values.
override fun toString(): String A shape summary; toText is the one that prints the contents.

visualizers.kt

Grouped controls whose value is more than one number: multisliders, matrices, range sliders, menus and recorded gestures.

They are named after the Max/MSP objects they borrow from: multislider (multiSlider), matrixctrl (matrix), rslider (rangeSlider) and umenu (menu). All four travel the same way — the whole value as a list of doubles in one array-update message (ArrayControlUpdate) — and are cached as comma-separated text in ControllerControl.text, so a rerun restores them like any other grouped control. The pointer handling is src/commonMain/resources/arrayControls.js.

The multislider, matrix and range slider also have a playhead: a position the box writes and the person only sees. It is pushed like a meter reading (MeterHandle: rate-limited, in memory, never cached, never calling back), so a sequencer can drive it from its clock every tick.

gesture records a pointer path as a looping take (GestureHandle); ArcRange and arcContains answer range questions on a circular dial.

src/jvmMain/kotlin/afield/ui/visualization/visualizers.kt — read the source

Used in: Visualizers, Displays, and Little Instruments, Field from your phone

Top-level functions

Declaration Summary
fun ControlGroupBuilder.multiSlider(label: String, count: Int = 16, min: Double = 0.0, max: Double = 1.0, step: Double = 0.0, initialValues: List<Double> = List(count) { min }, height: Int = 96, polar: Boolean = false, size: Int = 240, onUpdate: (List<Double>) -> Unit): ArrayControlHandle<List<Double>> Adds a bank of count vertical sliders, edited by sweeping across them — Max's multislider.
fun ControlGroupBuilder.matrix(label: String, rows: Int = 4, columns: Int = 8, initialValue: List<List<Boolean>> = emptyList(), polar: Boolean = false, size: Int = 240, onUpdate: (List<List<Boolean>>) -> Unit): ArrayControlHandle<List<List<Boolean>>> Adds a grid of toggles, painted by dragging — Max's matrixctrl.
fun ControlGroupBuilder.matrix(label: String, rows: Int = 4, columns: Int = 8, states: Int, initialValue: List<List<Int>> = emptyList(), polar: Boolean = false, size: Int = 240, onUpdate: (List<List<Int>>) -> Unit): ArrayControlHandle<List<List<Int>>> Adds a grid whose cells each hold one of states values, 0 until states.
fun ControlGroupBuilder.rangeSlider(label: String, min: Double = 0.0, max: Double = 1.0, step: Double = 0.0, initialValue: ClosedFloatingPointRange<Double> = min..max, polar: Boolean = false, size: Int = 240, onUpdate: (ClosedFloatingPointRange<Double>) -> Unit): ArrayControlHandle<ClosedFloatingPointRange<Double>> Adds a slider with two thumbs selecting an interval — Max's rslider.
fun ControlGroupBuilder.menu(label: String, options: List<String>, initialIndex: Int = 0, onUpdate: (index: Int, option: String) -> Unit): ControlValueHandle<Int> Adds a drop-down choice among options — Max's umenu.
fun ClosedFloatingPointRange<Double>.arcContains(value: Double): Boolean Whether value lies on this interval as a polar rangeSlider means it: clockwise from start to endInclusive, wrapping through the top when start > endInclusive. An ordinary range answers exactly as in does.
fun ControlGroupBuilder.gesture(label: String, xRange: Pair<Double, Double> = -1.0 to 1.0, yRange: Pair<Double, Double> = -1.0 to 1.0, initialValue: Vector2f = Vector2f(0f, 0f), initialTake: List<Pair<Double, Vector2f>> = emptyList(), size: Int = 240, pen: Boolean = false, onUpdate: (Vector2f) -> Unit): GestureHandle Adds an XY pad that records what you do with it — a gesture you can loop.

ArrayControlHandle

class ArrayControlHandle<T> internal constructor(private val valueHandle: ControlValueHandle<T>, private val playheadHandle: MeterHandle)

What multiSlider, matrix and rangeSlider return: the control's value, and its playhead.

Declaration Summary
val controlId: String Stable id of the control, shared with the tablet surface.
fun set(value: T): Boolean Shows value on the control without calling its callback. See ControlValueHandle.set.
fun playhead(position: Double): Boolean Moves the playhead to position — a bar or column index for a multislider or matrix (the fractional part is ignored), a value on the track for a range slider. A negative position on a multislider or matrix hides it. Returns false when the update was rate-limited away.
fun playhead(index: Int): Boolean playhead for an integer index.
fun hidePlayhead(): Boolean Hides the playhead.

ArcRange

class ArcRange(override val start: Double, override val endInclusive: Double) : ClosedFloatingPointRange<Double>

An interval on a circle, as a polar rangeSlider reports it: clockwise from start to endInclusive, wrapping through the top when start > endInclusive. value in arc and isEmpty follow the arc, where an ordinary 1700.0..400.0 would read as empty.

Declaration Summary
override fun lessThanOrEquals(a: Double, b: Double): Boolean
override fun contains(value: Double): Boolean
override fun isEmpty(): Boolean
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
override fun toString(): String

GestureHandle

class GestureHandle internal constructor(private val control: ArrayControlHandle<List<Double>>, take: List<Double>, private val sanitize: (List<Double>) -> List<Double> = { it }, val pen: Boolean = false, val xRange: Pair<Double, Double> = -1.0 to 1.0, val yRange: Pair<Double, Double> = -1.0 to 1.0)

What gesture returns: the recorded take, and a way to replay it.

Declaration Summary
val pen: Boolean True for gesture(pen = true): the take keeps pressure, tilt and twist with each position.
val xRange: Pair<Double, Double> The pad's value at its left and right edges.
val yRange: Pair<Double, Double> The pad's value at its bottom and top edges.
val controlId: String Stable id of the control, shared with the phone.
val hasTake: Boolean True once a take has been recorded (or given as initialTake).
val duration: Double Length of the take in seconds; 0.0 with no take.
fun at(seconds: Double): Vector2f? Where the take is seconds into its loop, interpolated between the recorded samples, or null with no take. Seconds past the end wrap round, so at(clock) loops forever.
fun playhead(seconds: Double): Boolean Shows a dot on the pad where the take is seconds into its loop. Negative hides it.
val samples: List<Pair<Double, Vector2f>> The take as (seconds, position) pairs in time order — the form initialTake accepts.
val penSamples: List<PenSample> The take with everything the pointer reported at each sample. A take recorded without pen has none of it: pressure is NaN and tilt and twist 0.
fun penLane(dimension: String, tolerance: Double = 0.02): List<Keyframe> One of the pen's dimensions — "pressure", "tiltX", "tiltY" or "twist" — reduced to a linear lane the way toKeyframes reduces x and y: the fewest samples that stay within tolerance (in that dimension's units: pressure 0..1, degrees for the rest) of the take. Empty for a take recorded without pen.
fun toKeyframes(tolerance: Double = 0.02): GestureLanes The take reduced to two linear lanes, x and y, sharing their key times: the fewest recorded samples that keep both axes within tolerance (pad units) of the take at every moment. The lanes start at 0 and end at duration, so they loop as the take does. See Box.bakeGesture.
fun fit(x: KeyframeEvaluator, y: KeyframeEvaluator): GestureFit How closely x and y follow this take; see GestureFit.
fun setTake(samples: List<Pair<Double, Vector2f>>) Replaces the take with samples, as if it had been recorded: the pad shows the new trail and the take is saved with the box. Positions are clamped to the pad and time starts at 0.
fun setTake(samples: List<PenSample>) Replaces the take with samples, pressure, tilt and twist included (a pen take keeps them).
fun setTake(x: KeyframeEvaluator, y: KeyframeEvaluator, samplesPerSecond: Double = 60.0) Replaces the take with the curves x and y — lanes edited in the keyframe editor — so the edited movement replays through the pad. See lanesToTake.

PenSample

data class PenSample(val t: Double, val x: Double, val y: Double, val pressure: Double = Double.NaN, val tiltX: Double = 0.0, val tiltY: Double = 0.0, val twist: Double = 0.0)

One sample of a gesture take with what the pointer reported besides its position, as the browser's PointerEvent gives it: pressure 0 to 1 (a mouse with a button down reports 0.5), tiltX and tiltY the pen's lean in degrees toward +x (right) and toward the user (down the screen), -90 to 90, and twist its turn about its own axis, 0 to 359 degrees. A take recorded without pen has NaN pressure and no tilt or twist.

Declaration Summary
val t: Double, val x: Double, val y: Double
val pressure: Double