Skip to content

Graphics

Everything that ends up as an FLine, a colour or a shader: drawing helpers, contouring, geometry, colour spaces, keyframing and the small shading languages' host side.

330 source file(s), 8046 public symbol(s), 5296 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.

gestureKeyframes.kt

Gesture takes and keyframe lanes, in both directions: bakeGesture turns a take recorded on a pad into two editable lanes, and lanesToTake renders lanes back into a take a pad accepts.

A take recorded by gesture(...) is a run of (t, x, y) samples that GestureHandle.at joins with straight lines. Baking keeps the fewest of those samples that stay within a tolerance of the take, as linear keys in two lanes that share their key times. Because the keys are a subset of the samples and both are joined by straight lines, the largest difference anywhere in time is the largest difference at a sample, so the tolerance holds at every t, not only at the samples.

src/jvmMain/kotlin/afield/animation/gesture/gestureKeyframes.kt — read the source

Used in: The keyframe editor: drawing a value over time

Top-level functions

Declaration Summary
fun reduceTakeIndices(take: DoubleArray, tolerance: Double): List<Int> The indices of the samples of take (flattened (t, x, y) triples) to keep so that neither axis strays more than tolerance from the take: Ramer–Douglas–Peucker over both axes at once, measured vertically (at the same time), which is how a lane is evaluated. The first and last samples are always kept, so the lanes span the take's whole loop.
fun takeToLanes(take: DoubleArray, tolerance: Double): GestureLanes Bakes take (flattened (t, x, y) triples) into two linear lanes; see reduceTakeIndices.
fun measureGestureFit(take: DoubleArray, x: KeyframeEvaluator, y: KeyframeEvaluator, between: Int = 8): GestureFit Measures x and y against take: at every sample time and at between evenly spaced moments between neighbouring samples, the worse axis's difference.
fun lanesToTake(x: KeyframeEvaluator, y: KeyframeEvaluator, samplesPerSecond: Double = 60.0): List<Pair<Double, Vector2f>> Renders two lanes into a take that gesture(initialTake = ...) or GestureHandle.setTake accepts: samplesPerSecond evenly spaced samples from the first key to the last of either lane, plus every key time, so corners stay sharp and bezier segments come out as smooth runs of short lines. Time starts at the earlier lane's first key; a take always starts at 0, so it is shifted there.
fun Box.bakeGesture(pad: GestureHandle, tolerance: Double = 0.02, xName: String = "x", yName: String = "y", channel: String? = null): Pair<KeyframeEvaluator, KeyframeEvaluator> Bakes the take recorded on pad into two editable lanes, x and y by default, shown in this box's keyframe editors and saved with it. The lanes share a time axis and their key times, so a Cmd-drag across a moment selects it in both. tolerance is in pad units: no point of the lanes is further than that from the take, on either axis. Returns the two lanes; their keys replace whatever the lanes held. With no take recorded, the lanes are left alone.

GestureFit

data class GestureFit(val maxError: Double, val meanError: Double, val keyCount: Int, val sampleCount: Int)

How well a pair of lanes matches a take: errors in pad units, worst axis at each moment.

Declaration Summary
val maxError: Double Largest difference between take and lanes, over a dense resample of the take's length.
val meanError: Double Mean of that difference over the same resample.
val keyCount: Int Keys in each lane (the lanes share their key times).
val sampleCount: Int Samples in the take.
override fun toString()

GestureLanes

data class GestureLanes(val x: List<Keyframe>, val y: List<Keyframe>)

The x and y lanes baked from a take.

keyframeInterpolants.kt

Custom key types for the keyframe editor that keyframes(...) opens: a named segment shape, written as a Kotlin lambda.

interpolant("overshoot") { v0 + (v1 - v0) * (1 + 2.7 * (u - 1).pow(3) + 1.7 * (u - 1).pow(2)) }

The name becomes a key type, offered in the key menu, and a key of that type shapes the segment that leaves it. The interpolant macro (macro_interpolant) keeps the lambda's source beside the live lambda and saves it with the box, in its keyframeInterpolants property. Compiling that source again is what keeps a curve's shape while the definition has not run: when the box defines the type only after it calls keyframes, and when the definition has been deleted.

src/jvmMain/kotlin/afield/animation/interpolation/keyframeInterpolants.kt — read the source

Top-level functions

Declaration Summary
fun Box.interpolant(name: String, block: KeySegment.() -> Double) Defines a key type named name for this box's keyframe editors: a segment shape, written as the value the curve should have at each point of the segment.
fun Box.registerInterpolant(name: String, source: String, block: KeySegment.() -> Double): KeyInterpolant What the interpolant macro rewrites a definition to: the lambda, and its source to save.
fun Box.macro_interpolant(args: List<String>): Insert? The interpolant("name") { body } macro: keeps the body's source next to the lambda.

KeySegment

class KeySegment internal constructor(val t: Double, val u: Double, val t0: Double, val t1: Double, val v0: Double, val v1: Double, val before: Keyframe? = null, val after: Keyframe? = null)

One segment of a keyframe curve, as an interpolant sees it: the receiver of its lambda.

Declaration Summary
val t: Double The time being evaluated, between t0 and t1.
val u: Double How far through the segment t is: (t - t0) / (t1 - t0), from 0 to 1.
val t0: Double The time of the key the segment leaves.
val t1: Double The time of the next key.
val v0: Double The value of the key the segment leaves.
val v1: Double The value of the next key.
val before: Keyframe? The key before this segment's first, if there is one: for shapes that look past the segment.
val after: Keyframe? The key after this segment's second, if there is one.
val span: Double The segment's length in time.
fun lerp(f: Double): Double The value f of the way from v0 to v1: lerp(u) is a straight line, lerp(f(u)) an easing.
fun lerp(a: Double, b: Double, f: Double): Double a + (b - a) × f.
fun smoothstep(f: Double): Double The smooth step from 0 to 1 as f goes from 0 to 1, with zero slope at both ends.

KeyInterpolant

class KeyInterpolant internal constructor(val name: String, val boxId: String, val source: String, @Volatile private var block: (KeySegment.() -> Double)?, val entry: KeyTypeEntry? = null)

A named key type: its lambda, and the Kotlin source it was written as.

Declaration Summary
val name: String The name a key stores to select this type.
val boxId: String The box that defined it, and the box its source is compiled back into. Empty for a library type.
val source: String The lambda's Kotlin source, saved with the box. Empty when there is none to save: a library type, or a definition the interpolant macro did not rewrite.
val entry: KeyTypeEntry? Where the type sits in the key menu; KeyTypeLibrary entries say, a box's own are "This box".
fun evaluate(s: KeySegment): Double The value of segment s under this type. A definition that cannot run gives the straight line.

KeyframeControls.kt

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

Used in: Visualizers, Displays, and Little Instruments

Top-level functions

Declaration Summary
fun KeyframeScope.slider(initial: Double, range: ClosedFloatingPointRange<Double> = 0.0..1.0, label: String? = null, step: Double = 0.01, law: ControlLaw = ControlLaw.LINEAR, lawExponent: Double = 2.0, lawCenter: Double = (range.start + range.endInclusive) / 2, significantDigits: Int = defaultSignificantDigits(law)): KeyframeControl<Double> A keyframed Double slider as a delegated local: var size by keys.slider(0.4, 0.1..1.0). The range, step and law are the ones keyframeSlider takes; label defaults to the property name.
fun KeyframeScope.slider(initial: Float, range: ClosedFloatingPointRange<Double> = 0.0..1.0, label: String? = null, step: Double = 0.01, law: ControlLaw = ControlLaw.LINEAR, lawExponent: Double = 2.0, lawCenter: Double = (range.start + range.endInclusive) / 2, significantDigits: Int = defaultSignificantDigits(law)): KeyframeControl<Float> slider for a Float local, through keyframeFloatSlider: var gain by keys.slider(0.35f, 0.0..4.0).
fun KeyframeScope.checkbox(initial: Boolean, label: String? = null): KeyframeControl<Boolean> A keyframed checkbox as a delegated Boolean local, through keyframeCheckbox.
fun KeyframeScope.vec2(initial: Vector2f, xRange: ClosedFloatingPointRange<Double> = -1.0..1.0, yRange: ClosedFloatingPointRange<Double> = -1.0..1.0, label: String? = null, step: Double = 0.01): KeyframeControl<Vector2f> A keyframed XY pad as a delegated Vector2f local, through keyframeVec2Control.
fun KeyframeScope.color(initial: Vector4f, label: String? = null): KeyframeControl<Vector4f> A keyframed RGBA colour picker as a delegated Vector4f local, through keyframeColorPicker.
fun KeyframeScope.text(initial: String, label: String? = null): KeyframeControl<String> A keyframed text field as a delegated String local, through keyframeText. Keys step between strings.

KeyframeControl

class KeyframeControl<T> internal constructor(private val scope: KeyframeScope, initial: T, private val explicitLabel: String?, private val copy: (T) -> T, private val controlType: String, private val restore: (ControllerControl) -> T?, private val register: KeyframeScope.(name: String, get: () -> T, set: (T) -> Unit) -> Unit, private val build: ControlGroupBuilder.(control: KeyframeControl<T>) -> ((T) -> Unit)) : ReadWriteProperty<Any?, T>

One keyframed control declared as a delegated local:

Declaration Summary
var name: String The key name — the delegated property's name, once declared.
val label: String The control's label: the label argument, or the property name.
var value: T The current value; the same thing reading the delegated local returns.
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): KeyframeControl<T>
override fun getValue(thisRef: Any?, property: KProperty<*>): T
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T)

keyframeCodeBlend.kt

Blending two code keyframes by finding the structure they share and interpolating the numbers they do not.

A KeyframeScope can snapshot code as well as values: a stack.auto StackSL program through KeyframeScope.stackAuto, and a Kotlin expression through the sourceBlock { } macro. Two such snapshots have no midpoint in general — half a program is not a program — so KeyframeScope.interpolate steps them, taking the right-hand source once t >= 1. That is all that is available when the two programs disagree about what they do.

When they agree about what they do and differ only in numbers, there is a real midpoint, and this file finds it. Both sources are lexed, the two token streams are aligned, and every place where the alignment pairs a number with a number becomes a CodeBlendSite. Anything the alignment cannot pair is a CodeBlendDivergence, and one divergence is enough to put the whole slot back on the stepping path: a program built from the left structure with the right numbers, where the two structures differ, is neither keyframe at either end.

The two ways to spend the blend

CodeBlendMode.STATIC writes the interpolated numbers into the source and hands back a new program, which the slot then compiles. It reaches every site — including StackSL's modifier chains and random ranges, whose grammar demands literals — and it costs a shader compile (or, for Kotlin, a K2 compile) per value of t. Scrubbing a timeline or stepping between two looks is what it is for; sixty times a second is not.

CodeBlendMode.RUNTIME emits one program in which each moving site reads the blend from outside. The program is then invariant in t, so it compiles once and animates for free. StackSL gets there by rewriting 0.5 as 0.2 0.9 ?pc.keyframeBlend .x mix — a push of each endpoint and the builtin mix, which leaves one value on the stack exactly as the literal it replaced did, so the surrounding stack arithmetic is untouched. Kotlin gets there by rewriting 0.5f as afield.animation.keyframes.kfMix(0.2f, 0.9f, ...). Sites a mix expression cannot reach — see StackSLNumericFormKind — make the plan report CodeBlendPlan.runtimeCapable false, and a RUNTIME request over one of those falls back to STATIC and says so.

The push-constant convention

A StackSL program can only read what the host has already put in the shader, and a stack.auto slot cannot declare a uniform of its own. So the blend parameter arrives by convention: declare a vec4 called keyframeBlend in the shader's push-constant block, and register the matching keyframe lane so the scope can write it.

layout(push_constant) uniform Controls {
    vec4 keyframeBlend;
} pc;
scope.target("keyframeBlend") { module.current.liveUniformStructures["pc"] }
scope.codeBlend = CodeBlendOptions(mode = CodeBlendMode.RUNTIME)

KeyframeScope.interpolate then fills that lane with keyframeBlendVector on every blend, and the four components are four shapes of the same parameter — see CodeBlendLane. A vec4 rather than a float because a push-constant block is padded to vec4 boundaries anyway, and because a program that wants the eased blend for one number and the raw one for another should not need a second uniform to say so.

The names are defaults (keyframeBlendPushConstant, keyframeBlendKey); a shader whose block instance is called something other than pc sets CodeBlendOptions.stackSLRead to match.

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

Used in: Keyframes: capturing, persisting and replaying live state

Top-level functions

Declaration Summary
fun keyframeBlendVector(t: Double): Vector4f The blend parameter as the vec4 the push-constant convention expects: the same t in the four shapes of CodeBlendLane.
fun keyframeBlendAmount(id: Int, component: Int = CodeBlendLane.RAW.component): Double Read blend slot id's parameter on component component — the function a CodeBlendMode.RUNTIME Kotlin blend calls, once per number, every time the block runs.
fun kfMix(a: Double, b: Double, t: Double): Double Blend two Doubles linearly — the generated call behind a blended Kotlin Double literal.
fun kfMix(a: Float, b: Float, t: Double): Float Blend two Floats linearly, staying a Float so a blended 1.5f still fits a Float uniform.
fun kfMix(a: Int, b: Int, t: Double): Int Blend two Ints and round, so a blended Int literal is still an Int.
fun kfMix(a: Long, b: Long, t: Double): Long Blend two Longs and round, for a literal written with an L.
fun kfMix(a: Vector2fc, b: Vector2fc, t: Double): Vector2f Blend two vec2s component by component, into a fresh vector.
fun kfMix(a: Vector3fc, b: Vector3fc, t: Double): Vector3f Blend two vec3s component by component, into a fresh vector.
fun kfMix(a: Vector4fc, b: Vector4fc, t: Double): Vector4f Blend two vec4s component by component, into a fresh vector.
fun kfMix(a: Quaternionf, b: Quaternionf, t: Double): Quaternionf Blend two quaternions along the shorter arc, into a fresh quaternion.
fun kfMix(a: DualQuaternionf, b: DualQuaternionf, t: Double): DualQuaternionf Blend two rigid transforms as a screw motion, into a fresh dual quaternion.
fun planStackSLCodeBlend(left: String, right: String, options: CodeBlendOptions = CodeBlendOptions()): CodeBlendPlan Align two StackSL programs and report what a blend between them would do.
fun planKotlinCodeBlend(left: String, right: String, options: CodeBlendOptions = CodeBlendOptions()): CodeBlendPlan Align two Kotlin expressions and report what a blend between them would do.

Top-level values

Declaration Summary
const val keyframeBlendPushConstant: String The conventional StackSL read expression for the blend push constant: a vec4 keyframeBlend member of a push-constant block whose instance is called pc.
const val keyframeBlendKey: String The default keyframe lane name the blend vec4 is written to: keyframeBlend.

CodeBlendLanguage

enum class CodeBlendLanguage

Which of the two languages a CodeBlendPlan was built from.

Constants: STACKSL, KOTLIN.

CodeBlendMode

enum class CodeBlendMode

What KeyframeScope.interpolate should do when both sides of a slot are code.

Constants: STEP, STATIC, RUNTIME.

CodeBlendLane

enum class CodeBlendLane(val component: Int, val swizzle: String)

Which component of the blend vec4 a runtime-blended program reads.

Constants: RAW, CLAMPED, SMOOTH, COMPLEMENT.

Declaration Summary
val component: Int The component index in the blend vec4, and the offset keyframeBlendAmount reads.
val swizzle: String The StackSL swizzle word that selects this component.

CodeBlendOptions

data class CodeBlendOptions(val mode: CodeBlendMode = CodeBlendMode.STEP, val lane: CodeBlendLane = CodeBlendLane.RAW, val stackSLRead: String = keyframeBlendPushConstant, val blendKey: String = keyframeBlendKey)

How a scope should blend code: which CodeBlendMode, which CodeBlendLane, and the two names the push-constant convention needs.

Declaration Summary
val mode: CodeBlendMode What to do with two code values. Defaults to CodeBlendMode.STEP, the pre-existing behaviour.
val lane: CodeBlendLane Which component of the blend vec4 a runtime-blended program reads.
val stackSLRead: String The GLSL expression a runtime-blended StackSL program reads the blend from.
val blendKey: String The keyframe lane KeyframeScope.interpolate writes keyframeBlendVector to, when registered.

CodeBlendInterpolation

enum class CodeBlendInterpolation

How the numbers of one CodeBlendSite are interpolated.

Constants: LERP, SLERP, SCLERP.

CodeBlendSiteKind

enum class CodeBlendSiteKind(val interpolation: CodeBlendInterpolation, val runtimeCapable: Boolean)

What kind of thing one blended site is, which decides both how its numbers move and whether a runtime blend can reach it.

Constants: NUMBER, VECTOR, QUATERNION, DUAL_QUATERNION, RANGE, MODIFIER.

Declaration Summary
val interpolation: CodeBlendInterpolation How this site's numbers are interpolated.
val runtimeCapable: Boolean Whether the site can be rewritten as a mix expression the program evaluates every frame.

CodeBlendSite

class CodeBlendSite internal constructor(val kind: CodeBlendSiteKind, val start: Int, val end: Int, val leftText: String, val rightText: String, val left: List<Double>, val right: List<Double>, internal val skeleton: List<String>, internal val integral: List<Boolean>, internal val numberTexts: List<String>)

One place two code keyframes hold numbers in the same position — the unit a code blend interpolates.

Declaration Summary
val kind: CodeBlendSiteKind What the site is, and so how it blends.
val start: Int Start of the site's span in the left source, inclusive.
val end: Int End of the site's span in the left source, exclusive.
val leftText: String The site exactly as the left source writes it.
val rightText: String The site exactly as the right source writes it.
val left: List<Double> The numbers the left source holds, in source order.
val right: List<Double> The numbers the right source holds, in the same order.
val moving: Boolean True when the two sides really differ, and so when a blend has anything to do here.
val runtimeCapable: Boolean Whether a runtime blend can reach this site; false for the literal-only StackSL forms.
fun blend(t: Double): List<Double> The site's numbers blended at t, by kind's interpolation.
fun staticText(t: Double): String The site rewritten with its numbers blended at t — the CodeBlendMode.STATIC substitution.
override fun toString(): String The left text, an arrow, the right text — one line of a breakdown.

CodeBlendDivergence

class CodeBlendDivergence internal constructor(val index: Int, val line: Int, val left: String, val right: String)

One place the alignment could not pair the two sources — the reason a slot is not blendable.

Declaration Summary
val index: Int Token index in the left stream where the two sources part company.
val line: Int Line number in the left source, 1-based, or 0 for a token only the right source has.
val left: String What the left source has here, or "" when it has nothing.
val right: String What the right source has here, or "" when it has nothing.
override fun toString(): String

CodeBlendPlan

class CodeBlendPlan internal constructor(val language: CodeBlendLanguage, val options: CodeBlendOptions, val leftSource: String, val rightSource: String, val sites: List<CodeBlendSite>, val divergences: List<CodeBlendDivergence>) : ToHTML

The alignment of two code keyframes: what they share, where they differ numerically, and where they differ structurally.

Declaration Summary
val language: CodeBlendLanguage Which language was aligned.
val options: CodeBlendOptions The options the plan was built under; a scope's plan cache is keyed on these.
val leftSource: String The left source, verbatim — what bake and runtimeSource rebuild from.
val rightSource: String The right source, verbatim.
val sites: List<CodeBlendSite> Every numeric site the alignment paired, moving or not, in left-source order.
val divergences: List<CodeBlendDivergence> Every token pair the alignment could not reconcile. Empty means the two structures agree.
val movingSites: List<CodeBlendSite> The sites that actually differ between the two sources.
val blendable: Boolean True when the two sources share one structure, so that a blend between them exists at all.
val runtimeCapable: Boolean True when every moving site can be rewritten as a mix, so CodeBlendMode.RUNTIME is available.
val effectiveMode: CodeBlendMode The mode this plan can actually deliver, which is CodeBlendOptions.mode unless something stopped it: a structural divergence drops any mode to CodeBlendMode.STEP, and a literal-only site drops CodeBlendMode.RUNTIME to CodeBlendMode.STATIC.
val downgraded: Boolean True when effectiveMode is not what was asked for, and so when the breakdown is worth reading.
val downgradeReason: String Why effectiveMode is not CodeBlendOptions.mode, or "" when it is.
fun bake(t: Double): String The left source with every moving site's numbers interpolated at t — a different program for every t, and the only mode that can reach a literal-only site.
val runtimeSource: String by lazy The one program a CodeBlendMode.RUNTIME blend runs: every moving site replaced by a mix of the two endpoints against the blend parameter.
val runtimeId: Int by lazy The registry slot a Kotlin runtime blend reads its parameter from, allocated on first use.
fun drive(t: Double) Publish t as this plan's runtime blend parameter, for a CodeBlendLanguage.KOTLIN blend to read on its next invocation. A no-op for StackSL, whose parameter travels as a push constant.
fun describe(): String The plan as text: the counts, the downgrade if any, then a line per moving site and divergence.
override fun toHTML(): String The breakdown as HTML: the header counts, a row per moving site with its midpoint, and a row per divergence.
override fun toString(): String One line: what was asked for, what will happen, and how many sites are in play.

keyframeSweep.kt

Overnight exploration of a keyframe space: a directory of saved keys in, an archive of renders out.

A KeyframeScope gives you one look at a time — capture it, edit it, put it back. A directory of saved keys is something else: a space, whose interesting points are mostly the ones nobody has visited. KeyframeSweep visits them while you sleep. Each pass draws a few keys from the pool, blends them at random weights (sometimes past either end of the hull), applies the blend to the live scope, makes a directory, and hands that directory to a suspend callback of yours which runs to completion before the next pass starts. What the callback writes there is its business — a still, a clip, a mesh, a CSV. What the sweep writes there is the provenance: the blended key as ordinary key JSON, and a manifest saying which keys went in at which weights.

The point of the archive is what happens to it afterwards. A night of this is a few thousand directories to categorise, cluster, score or hand to a model, and the sweep's job is to make that second pass possible: every render is self-describing, index.jsonl is one line per render for a reader that does not want to walk the tree, and renders/ is itself a valid keyframe pool — so tonight's output is a legitimate input for tomorrow night.

The blend is a fold, not new maths

KeyframeScope.interpolate already knows how every lane type blends — numbers and vectors lerp, quaternions slerp, CameraPose sclerp, code blends or steps. So an N-way blend here is a pairwise fold over it rather than a second type table that would immediately drift from the first: with weights sorted descending and running sums S, folding t = w / S at each step yields exactly the affine weighted mean for every linear lane, and inherits the rest for free.

Sorting descending is what makes it safe rather than merely neat. Weights sum to 1, so the running sums start at the largest weight and end at 1, rising while the terms are positive and falling once they go negative — the minimum is therefore min(w_max, 1), which is always positive. No division ever approaches zero, including under extrapolation, where some weights are negative.

Three things the fold alone gets wrong, and this file handles — see DiscreteBlend, MissingSlots and SweepConfig.preserveNumericKind. All three are recorded per render rather than assumed, so a later pass over the archive can see which of them applied to any given render.

What a night survives

Nothing here trusts the run to finish. A render's manifest is written before its callback and rewritten after, atomically, so a crashed pass is identifiable as one; SweepConfig.resume picks up where the process died and re-derives each render's randomness from seed + index, so skipping completed work cannot change what the unfinished work would have drawn. A throwing callback is one failed render, not a lost night — until SweepConfig.maxConsecutiveFailures of them in a row says the night is broken. And SweepConfig.minFreeBytes stops the sweep before the disk does.

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

Used in: Sweeping saved keyframes overnight

Top-level functions

Declaration Summary
fun dirichlet(rng: Random, n: Int, concentration: Double = 1.0): DoubleArray n weights summing to 1, drawn from a symmetric Dirichlet with concentration concentration.
fun readSweepArchive(out: Path): List<SweepRenderManifest> Every render in an archive, newest last — the offline pass's way in.
fun sweepArchiveSummary(out: Path): String A readout of what a night actually did: how many renders, how they ended, how wide the blends ranged, and which pool keys got used.
fun SweepReport.rendersPerHour(): Double The one number an overnight run is judged on: completed renders per hour.

Top-level values

Declaration Summary
const val sweepManifestFormat: Int Bumped when the archive layout changes in a way a reader of an older archive would notice.

DiscreteBlend

enum class DiscreteBlend

What a blend does with the lanes that have no midpoint: strings, StackSLString programs and SourceBackedBlock Kotlin.

Constants: ANCHOR, ROULETTE_PER_LANE, AS_INTERPOLATED.

MissingSlots

enum class MissingSlots

What a blend does when its participants disagree about which lanes exist.

Constants: FILL_FROM_LIVE, FILL_FROM_ANCHOR, INTERSECT, REQUIRE_ALL.

SweepConfig

data class SweepConfig(val seed: Long = 1L, val count: Int = Int.MAX_VALUE, val pattern: String = everyKeyGlob, val minArity: Int = 2, val maxArity: Int = 3, val concentration: Double = 1.0, val extrapolationProbability: Double = 0.15, val minExtrapolationGain: Double = 1.2, val maxExtrapolationGain: Double = 2.0, val discrete: DiscreteBlend = DiscreteBlend.ANCHOR, val missingSlots: MissingSlots = MissingSlots.FILL_FROM_LIVE, val preserveNumericKind: Boolean = true, val applyToScope: Boolean = true, val restoreScopeAtEnd: Boolean = true, val maxDurationMs: Long = 0, val minFreeBytes: Long = 8L shl 30, val maxConsecutiveFailures: Int = 5, val resume: Boolean = true, val keepFailedRenders: Boolean = true, val draws: List<SweepDraw> = emptyList())

Every knob a sweep has, as one serializable value — so the run's own configuration is recorded in sweep.json beside its output and a night is reproducible from its archive rather than from whatever the box said at the time.

Declaration Summary
val seed: Long Root of the run's randomness. Every render draws from seed and its own index, nothing else.
val count: Int How many renders to attempt. Left unbounded, maxDurationMs or the disk floor ends the run.
val pattern: String Which files under the pool directory are keys. See everyKeyGlob, and keyFilesMatching.
val minArity: Int Fewest keys in one blend. 1 is meaningful and worth allowing: a blend of one key is that key replayed exactly, which salts the archive with unmixed references to compare the blends against.
val maxArity: Int Most keys in one blend, clamped to the pool size.
val concentration: Double The Dirichlet concentration the weights are drawn from, and the single most consequential number here.
val extrapolationProbability: Double How often a blend is pushed outside the hull of its participants.
val minExtrapolationGain: Double Least push, measured in hull-crossings from the anchor.
val maxExtrapolationGain: Double Most push. Past about 3.0 most looks fall apart, which is occasionally the point.
val discrete: DiscreteBlend See DiscreteBlend.
val missingSlots: MissingSlots See MissingSlots.
val preserveNumericKind: Boolean Round each blended number back to the numeric type the anchor held it in.
val applyToScope: Boolean Write the blend to the live scope before calling the callback. Off makes the sweep a pure generator.
val restoreScopeAtEnd: Boolean Put the scope back the way it was found when the run ends, however it ends.
val maxDurationMs: Long Wall-clock budget in milliseconds; 0 for none. The "stop at 7am" knob.
val minFreeBytes: Long Stop while this many bytes are still free on the archive's filesystem. Eight gigabytes by default: an overnight sweep of GPU stills is a rate of disk consumption, and the thing that goes wrong is not the archive being large but the machine having nothing left at 4am.
val maxConsecutiveFailures: Int Consecutive failures that mean the night is broken rather than the render unlucky.
val resume: Boolean Skip renders a previous process already finished. See KeyframeSweep.run.
val keepFailedRenders: Boolean Keep the directory of a failed render, with its manifest and partial output, for forensics.
val draws: List<SweepDraw> Renders to make exactly as written, instead of drawing them at random.
val renders: Int How many renders the run attempts: count, or the number of draws when there are any.
val arity: IntRange minArity..maxArity, for reading.

SweepDraw

data class SweepDraw(val keys: List<String>, val weights: List<Double>, val pole: String? = null, val gain: Double? = null, val anchor: String? = null)

One render chosen rather than drawn: which keys take part, their convex weights, and optionally a pole to push away from and how far.

Declaration Summary
val keys: List<String> Keys by their path relative to the pool, as SweepParent.key names them.
val weights: List<Double> One non-negative weight per key, normalised before use.
val pole: String? The key to push away from, one of keys; null for a convex blend.
val gain: Double? Hull-crossings to push; at least 1.0, and given exactly when pole is.
val anchor: String? Whose discrete lanes and numeric kinds the blend takes; null for the largest final weight.

SweepParent

data class SweepParent(val key: String, val weight: Double, val schemaVersion: Int, val filled: List<String> = emptyList())

One key's part in one blend.

Declaration Summary
val key: String Path relative to the pool root, / separated — the pool's own identity for the key.
val weight: Double Its weight in the affine blend. Sums to 1 across a render's parents; negative under extrapolation.
val schemaVersion: Int schemaVersion as the file carried it. More than one across a render's parents means schema drift.
val filled: List<String> Lanes this key did not have, filled per MissingSlots. Empty is the healthy case.

SweepArtifact

data class SweepArtifact(val kind: String, val file: String? = null, val bytes: Long = 0, val note: String? = null)

What a render's callback left behind, as recorded by SweepRender.artifact.

Declaration Summary
val kind: String Free-form: still, clip, frames, whatever the callback calls it.
val file: String? Path relative to the render directory, or null when the artifact could not be produced.
val bytes: Long
val note: String? Why it is missing, when file is null. Never a fabricated file; always a stated gap.

SweepRenderManifest

data class SweepRenderManifest(val format: Int = sweepManifestFormat, val index: Int, val id: String, val seed: Long, val status: String, val startedAt: String, val finishedAt: String? = null, val durationMs: Long? = null, val parents: List<SweepParent>, val anchor: String, val extrapolationPole: String? = null, val extrapolationGain: Double? = null, val discreteLanes: Map<String, String> = emptyMap(), val droppedLanes: List<String> = emptyList(), val laneCount: Int = 0, val artifacts: List<SweepArtifact> = emptyList(), val error: String? = null)

One render, completely described — the file an offline pass reads to decide what it is looking at.

Declaration Summary
val format: Int
val index: Int
val id: String
val seed: Long The per-render seed, derived from SweepConfig.seed and index. Redraws this render exactly.
val status: String running, ok, failed or cancelled.
val startedAt: String
val finishedAt: String?
val durationMs: Long?
val parents: List<SweepParent> The keys that went in, and at what weight.
val anchor: String The parent that won the discrete lanes, and supplies numeric types under preserveNumericKind — drawn by roulette over the final weights, so it is usually the key that dominates the picture.
val extrapolationPole: String? The parent this blend was pushed away from, for an extrapolated render — a different key from anchor, which is where the discrete lanes and numeric types come from. One key in both roles would push a blend away from the look it then inherits.
val extrapolationGain: Double? How far outside the hull this blend sits, in hull-crossings from the anchor — 1.0 is exactly on the far face, 1.5 is half as far again beyond it. Null for an ordinary convex blend.
val discreteLanes: Map<String, String> Discrete lane name to the parent it came from — the provenance the fold could not be trusted with.
val droppedLanes: List<String> Lanes present in the participants that the blend could not produce, usually a type disagreement.
val laneCount: Int How many lanes the blended key carries.
val artifacts: List<SweepArtifact>
val error: String? The callback's failure, first line only. Null unless status is failed.

SweepRunManifest

data class SweepRunManifest(val format: Int = sweepManifestFormat, val config: SweepConfig, val pool: String, val out: String, val startedAt: String, val finishedAt: String? = null, val poolSize: Int = 0, val ineligible: Int = 0, val scopeSlots: List<String> = emptyList(), val poolProblems: List<String> = emptyList(), val poolSkipped: String? = null, val completed: Int = 0, val failed: Int = 0, val skipped: Int = 0, val stoppedBecause: String? = null)

The run as a whole: what it was asked to do, against what it found. Written to sweep.json.

Declaration Summary
val format: Int
val config: SweepConfig
val pool: String
val out: String
val startedAt: String
val finishedAt: String?
val poolSize: Int Pool keys eligible to be blended.
val ineligible: Int Pool keys excluded by MissingSlots.REQUIRE_ALL.
val scopeSlots: List<String> Lane names the live scope watched when the run started.
val poolProblems: List<String> KeyframeCollection's per-file complaints — damaged keys the pool contains.
val poolSkipped: String? KeyframeCollection.skipSummary(): what the glob hid, and what was not a key at all.
val completed: Int
val failed: Int
val skipped: Int
val stoppedBecause: String?

SweepReport

data class SweepReport(val out: Path, val completed: Int, val failed: Int, val skipped: Int, val ineligible: Int, val poolSize: Int, val elapsedMs: Long, val stoppedBecause: String)

How a run ended, returned by KeyframeSweep.run and also the tail of sweep.json.

Declaration Summary
val out: Path
val completed: Int Renders whose callback returned normally.
val failed: Int Renders whose callback threw. Their directories survive unless configured otherwise.
val skipped: Int Renders a previous process had already finished.
val ineligible: Int Pool keys excluded by MissingSlots.REQUIRE_ALL.
val poolSize: Int
val elapsedMs: Long
val stoppedBecause: String count, duration, disk, failures or cancelled — why the loop stopped. Worth printing in the morning: disk and failures mean the night was cut short, and the render count alone does not say which.
override fun toString() The one line worth printing in the morning: the counts, and why the run stopped.

SweepRender

class SweepRender internal constructor(val index: Int, val id: String, val dir: Path, val key: KeyframeScope.Key, val seed: Long, manifest: SweepRenderManifest)

The handle a sweep's callback is given: where to write, what it is rendering, and how it got here.

Declaration Summary
val index: Int Zero-based position in the run. Stable across a SweepConfig.resume.
val id: String 000123-4f2a9c, the directory's name: ordered by index, and distinct per seed.
val dir: Path The render's own directory, created before the callback is called.
val key: KeyframeScope.Key The blended key. Also written to key.json in dir before the callback runs.
val seed: Long This render's seed — for a callback that wants its own reproducible randomness.
var manifest: SweepRenderManifest The provenance record, as it will be written. artifact is the one part a callback adds.
fun file(name: String): Path A path inside dir. Creates nothing; the callback writes it.
fun artifact(kind: String, file: Path? = null, note: String? = null) Record something the callback produced, so the offline pass can find it without guessing file names.

KeyframeSweep

class KeyframeSweep(val scope: KeyframeScope, val pool: Path, val out: Path, val config: SweepConfig = SweepConfig(), val onRender: (SweepRenderManifest) -> Unit = {})

A random walk of a keyframe space, with a render at every stop.

Declaration Summary
val scope: KeyframeScope The live scope. Supplies the target set, the blend maths, and (by default) the fill values.
val pool: Path Directory of saved keys to draw from. Read once, non-destructively.
val out: Path Archive root. Created if absent; added to if it already holds a run.
val config: SweepConfig
val onRender: (SweepRenderManifest) -> Unit Called after every terminal render, completed or failed, with the manifest as written. The place to put progress reporting — a display table, a line in a log, a push to a dashboard.
val rendersDir: Path out/renders, one directory per render. Itself a valid keyframe pool, via the key.json in each.
val indexFile: Path out/index.jsonl: one compact SweepRenderManifest per line, for a reader that will not walk the tree.
val runManifestFile: Path out/sweep.json: the run's own configuration and pool census.
suspend fun run(body: suspend (SweepRender) -> Unit): SweepReport Run the sweep, calling body once per render and waiting for it each time.
fun reindex(): Int Rebuild index.jsonl from the manifest.json files under renders/, and answer how many lines it wrote.

keyframeSweepCapture.kt

The GPU half of a keyframe sweep: turning one blended look into the still and the short clip that a night's archive is actually made of.

KeyframeSweep is renderer-free: it blends, it records, it calls your callback. But most callbacks do the same two things, and they are the two things with traps in them: an offscreen readback that can come back empty, and a frame sequence that has to be muxed by a tool which may not be installed. So they live here once rather than in every sweep.

Both helpers follow the pattern the RectSL and StackSL gallery harnesses established: one renderer and one RenderToTexture for the whole run, reused across thousands of renders, with only the uniforms changing. Building a target per render is what makes an overnight sweep run out of device memory at 3am.

Neither helper will invent a file. A readback that comes back empty and a missing ffmpeg are both recorded as an artifact with a null path and a stated reason, and the frame PNGs are left on disk so the clip can be muxed later. An archive that says what it is missing can be analysed; one with a plausible-looking wrong file in it cannot.

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

Used in: Sweeping saved keyframes overnight

Top-level functions

Declaration Summary
suspend fun Renderer.awaitFrames(frames: Int = 1, timeoutMs: Long = 15_000L) Suspend until this renderer has presented frames more frames, or fail after timeoutMs.
suspend fun SweepRender.captureStill(renderer: Renderer, rtt: RenderToTexture, name: String = "still.png", settleFrames: Int = 2, kind: String = "still"): Path? Read rtt back and write it into this render's directory as a PNG, at full target resolution.
suspend fun SweepRender.captureClip(renderer: Renderer, rtt: RenderToTexture, frames: Int, fps: Int = 30, name: String = "clip.mp4", settleFrames: Int = 1, keepFrames: Boolean = true, kind: String = "clip", advance: suspend (frame: Int, t: Double) -> Unit): Path? Render a short clip of this blend: frames readbacks into a frames/ subdirectory, muxed to H.264 by ffmpeg.
fun muxSweepArchiveClips(out: Path, fps: Int = 30, name: String = "clip.mp4"): Int Mux every render in an archive that has frames but no clip — the morning repair for a night that ran without ffmpeg installed.

keyframeTidy.kt

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

keyframeTypeLibrary.kt

The key types every keyframe editor offers beyond its five built-in ones: the easing family, splines that look past the segment to the keys either side, shapes, and shaped noise. Each is a KeySegment lambda like the ones a box defines with interpolant(...), evaluated in Kotlin and drawn by the editor from samples of the segment.

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

KeyTypeEntry

class KeyTypeEntry internal constructor(val name: String, val label: String, val group: String, val family: String? = null, val variant: String? = null, val shape: KeySegment.() -> Double)

One entry in the library: the name a key stores, where it sits in the menu, and its shape.

Declaration Summary
val name: String The name a key stores to select this type, as ease.sine.in or shape.smoothstep.
val label: String How the type reads in the menu, as sine in or smoothstep.
val group: String The menu section the type sits in: Ease, Spline, Shape or Noise.
val family: String? For the easing table: the family, which is a row. Null for every other group.
val variant: String? For the easing table: the direction, which is a column. One of in, out and in-out.
val shape: KeySegment.() -> Double The shape itself: a KeySegment lambda returning the curve's value inside the segment.

KeyTypeLibrary

object KeyTypeLibrary

Declaration Summary
val entries: List<KeyTypeEntry> by lazy Every type in the library, built once: the easings first, then splines, shapes and noise.
fun named(name: String): KeyTypeEntry? The entry a key of type name uses, or null if no library type is called that.

keyframer.kt

Snapshot, interpolate and persist the live state of a running sketch. A KeyframeScope is a set of named targets — typed Kotlin locals, keyframe-aware controlGroup controls, contributors such as a score object, uniform Structure bindings, the uniform structures of a ModulePointer / ComputeModulePointer / MeshModulePointer, a stackAuto shader-source slot via KeyframeScope.stackAuto, or a Camera pose via KeyframeScope.camera. capture() reads them all into a KeyframeScope.Key; invoking that Key writes them all back.

KeyframeScope.interpolate blends two keys per value type: numbers and vectors lerp, quaternions slerp, dual quaternions and CameraPose sclerp, and strings / StackSLString / SourceBackedBlock step to the right-hand value at t >= 1. remap can re-time an individual key's t. The two code types can do better than stepping when the two sources differ only in numbers — see KeyframeScope.codeBlend and keyframeCodeBlend.kt.

Persistence goes through the KValue serial union, so only the types KValue.fromAny recognises survive a save — numbers (a Long exactly), Booleans, vec2/3/4, quaternion, dual quaternion, camera pose, string, StackSL source and source-backed block; anything else is left out and reported on error,keyframer. Every saveKey also writes a numbered copy into a <file>.versions directory with a manifest, which KeyframeScope.keyVersions lists and KeyframeScope.loadLatestKey follows. KeyframeScope.missingKey supplies defaults for keys absent from an older file, optionally per schema version.

Keys render themselves as HTML for box output (KeyDiff, diffHtml), and copyUpdateCodeToClipboard emits pasteable Kotlin — that one goes through the AWT clipboard, and on a headless JVM it logs and just returns the string.

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

Used in: Keyframes: capturing, persisting and replaying live state, Values over time: interpolants & keyframes

Top-level functions

Declaration Summary
fun stackSLString(value: String) StackSLString as a function, which is what Key.updateCode emits so its output can be pasted back.
fun ControlGroupBuilder.keyframeSlider(scope: KeyframeScope, name: String, label: String, min: Double = 0.0, max: Double = 1.0, step: Double = 0.01, law: ControlLaw = ControlLaw.LINEAR, lawExponent: Double = 2.0, lawCenter: Double = (min + max) / 2, significantDigits: Int = defaultSignificantDigits(law), get: () -> Double, set: (Double) -> Unit): ControlValueHandle<Double> Put one Double local in a scope and on a visible slider, with either direction keeping the other two in step. UI gestures call set; applying a key calls set and moves the visible control.
fun ControlGroupBuilder.keyframeFloatSlider(scope: KeyframeScope, name: String, label: String, min: Double = 0.0, max: Double = 1.0, step: Double = 0.01, law: ControlLaw = ControlLaw.LINEAR, lawExponent: Double = 2.0, lawCenter: Double = (min + max) / 2, significantDigits: Int = defaultSignificantDigits(law), get: () -> Float, set: (Float) -> Unit): ControlValueHandle<Double> keyframeSlider for the Float locals most graphics code uses.
fun ControlGroupBuilder.keyframeCheckbox(scope: KeyframeScope, name: String, label: String, get: () -> Boolean, set: (Boolean) -> Unit): ControlValueHandle<Boolean> A keyframed Boolean local and checkbox; the persisted lane is an interpolable 0/1 float.
fun ControlGroupBuilder.keyframeVec2Control(scope: KeyframeScope, name: String, label: String, xRange: Pair<Double, Double> = -1.0 to 1.0, yRange: Pair<Double, Double> = -1.0 to 1.0, step: Double = 0.01, get: () -> Vector2fc, set: (Vector2f) -> Unit): ControlValueHandle<Vector2f> A keyframed vec2 local and XY control.
fun ControlGroupBuilder.keyframeColorPicker(scope: KeyframeScope, name: String, label: String, get: () -> Vector4fc, set: (Vector4f) -> Unit): ControlValueHandle<Vector4f> A keyframed RGBA vec4 local and colour picker.
fun ControlGroupBuilder.keyframeText(scope: KeyframeScope, name: String, label: String, get: () -> String, set: (String) -> Unit): ControlValueHandle<String> A keyframed string local and compact text control.

Top-level values

Declaration Summary
val KeyframeScope.Key.h: String A key's KeyframeScope.Key.toHTML table, for display() and anywhere else HTML is what is wanted.

NumKind

enum class NumKind

Which Kotlin numeric type a saved number came from, so that KValue.toAny can hand back a Byte/Short/Int/Long/Float/Double rather than always a Double — a uniform bound to an Int has to come back as an Int.

Constants: BYTE, SHORT, INT, LONG, FLOAT, DOUBLE.

KValue

sealed interface KValue

The closed set of value types a KeyframeScope.Key can persist: numbers, vec2/3/4, quaternion, dual quaternion, camera pose, string, StackSL source and source-backed block. A key in memory holds plain Kotlin values; this is the serial union they are converted to on the way to disk and back.

Declaration Summary
data class Num(val kind: NumKind, val value: Double) : KValue Any Kotlin number, kept as a Double with the NumKind needed to restore its original type.
data class LongValue(val value: Long) : KValue A Long, exactly: a Double would lose it past 2^53.
data class Bool(val value: Boolean) : KValue A Boolean.
data class Vec2(val x: Float, val y: Float) : KValue A Vector2f, component by component.
data class Vec3(val x: Float, val y: Float, val z: Float) : KValue A Vector3f, component by component.
data class Vec4(val x: Float, val y: Float, val z: Float, val w: Float) : KValue A Vector4f, component by component — the usual RGBA or homogeneous-point uniform.
data class Quat(val x: Float, val y: Float, val z: Float, val w: Float) : KValue A Quaternionf as raw x, y, z, w; stored as-is, and not re-normalised on load.
data class DualQuat(val realX: Float, val realY: Float, val realZ: Float, val realW: Float, val dualX: Float, val dualY: Float, val dualZ: Float, val dualW: Float) : KValue A DualQuaternionf — a rigid transform (rotation plus translation) as its real and dual quaternion halves. Unlike Quat this is normalised on the way back in, so a slightly drifted saved value is repaired on load.
data class CameraPoseValue(val realX: Float, val realY: Float, val realZ: Float, val realW: Float, val dualX: Float, val dualY: Float, val dualZ: Float, val dualW: Float, val focusDistance: Float) : KValue A CameraPose: the camera's rigid transform as a dual quaternion, plus the focus distance that says how far in front of it the thing it is looking at sits. Also normalised on load.
data class StackSL(val value: String) : KValue StackSL source text, tagged as code rather than as a string so that it reloads as a StackSLString and interpolation steps it discretely instead of trying to blend it.
data class Str(val value: String) : KValue A plain String.
data class SourceBlock(val boxId: String, val source: String) : KValue A SourceBackedBlock reduced to the box it came from and its Kotlin source — the live lambda is not persisted. On load the block re-compiles its source in that box on first invocation, so a key restored into a document where the box is gone throws when the value is finally called.
fun toAny(): Any Rebuild the live Kotlin value this entry stands for — the load half of the round trip. A DualQuat and a CameraPoseValue are re-normalised on the way out, because a rigid transform that has drifted is a broken transform; a plain Quat is not (see its own note — it may be standing in for something that is not a rotation). Numbers regain their NumKind type, and a SourceBlock becomes a SourceBackedBlock that has not compiled anything yet.

KeyDTO

data class KeyDTO(val schemaVersion: Int = 0, val values: Map<String, KValue>)

What a saved key looks like on disk: a schema version and a name → KValue map, pretty-printed as JSON with type as the discriminator.

Declaration Summary
val schemaVersion: Int The owning scope's schemaVersion at the moment of saving. Nothing validates it on load: a mismatch is not an error, it only selects which KeyframeScope.missingKey defaults apply.
val values: Map<String, KValue> The key's entries by name. Values that KValue.fromAny could not represent are already gone.

KeyVersionInfo

data class KeyVersionInfo(val index: Int, val timestamp: String, val note: String? = null, val file: String)

One row of a key file's version history, as recorded in the <file>.versions/manifest.json that every saveKey appends to. KeyframeScope.keyVersions hands these back, sorted by index.

Declaration Summary
val index: Int Monotonic version number, starting at 0 and simply "highest so far plus one" — a save counter, not a schema version and not related to KeyDTO.schemaVersion.
val timestamp: String When the version was written, as an ISO-8601 Instant.now() string in UTC — or, for a row rebuilt from the file alone, that file's last-modified time.
val note: String? Whatever note was passed to saveKey, for telling two snapshots apart later. A row rebuilt from the file alone says "(recovered from disk; the manifest row for this version was lost)" here instead, since the real note lived only in the manifest that went missing.
val file: String File name inside the .versions directory, "%06d.json" of index.

StackSLString

data class StackSLString(val value: String)

A string that a KeyframeScope should treat as code rather than as text: StackSL source.

Declaration Summary
override fun toString(): String The StackSL source itself, so a StackSLString prints and concatenates like the string it wraps.

KeyframeContributor

fun interface KeyframeContributor

A domain object that contributes its state to an existing KeyframeScope.

Declaration Summary
fun registerKeyframes(scope: KeyframeScope, prefix: String)

KeyframeScope

class KeyframeScope(val schemaVersion: Int = 0)

A named set of live things worth snapshotting — uniform Structure bindings, a module's live uniforms, a stack.auto shader slot, a Camera — plus the machinery to capture, blend, save and reload them. This is the object you keep around for the lifetime of a sketch.

Declaration Summary
val targets The statically-bound targets, by key name, as registered by target(name, structure) — exposed so you can see and clear what a scope is watching.
val remap Per-key re-timing for interpolate: remap["gain"] = { t -> t * t * (3 - 2 * t) } eases that one value on a smoothstep while everything else stays linear.
var codeBlend: CodeBlendOptions What interpolate should do with a slot whose two sides are code — a StackSLString or a SourceBackedBlock.
fun codeBlendPlans(a: Key, b: Key): Map<String, CodeBlendPlan> The blend plan interpolate would use for every slot of a and b that holds code on both sides — the debugging view of a code blend, and what the keyframe browser shows.
inner class Key(val values: MutableMap<String, Any?>) : ToHTML One snapshot: names to values, and the ability to put them all back. Get one from capture, interpolate, loadKey, or by constructing it from a map you wrote by hand (as the docs examples do).
fun target(name: String, structure: Structure) Register a key name against a uniform structure held for good — the simplest target, for a Structure you built once and will keep.
fun target(name: String, structure: () -> Structure?) Register a key name against a structure that is looked up again on every read and write — the right choice for anything a shader reload can replace underneath you.
fun target(name: String, get: () -> Any?, set: (Any?) -> Unit) Register an arbitrary live value through explicit get and set functions. This is the escape hatch for score objects, simulation state and other things that are not shader Structures: capture calls get, and invoking a Key hands its stored value to set.
fun floatTarget(name: String, get: () -> Float, set: (Float) -> Unit) Register a float local without exposing an Any? setter at the call site.
fun doubleTarget(name: String, get: () -> Double, set: (Double) -> Unit) Register a double local without exposing an Any? setter at the call site.
fun booleanTarget(name: String, get: () -> Boolean, set: (Boolean) -> Unit) Register a Boolean local as an interpolable float lane. Applying crosses to true at 0.5; this is useful when a fade eventually switches a CPU-side path as well as a shader.
fun intTarget(name: String, get: () -> Int, range: IntRange = Int.MIN_VALUE..Int.MAX_VALUE, set: (Int) -> Unit) Register an integer local as a float lane, rounding and clamping when a key is applied.
fun vector2Target(name: String, get: () -> Vector2fc, set: (Vector2f) -> Unit) Register a vec2 local, copying at both boundaries so a captured key cannot alias it.
fun vector3Target(name: String, get: () -> Vector3fc, set: (Vector3f) -> Unit) Register a vec3 local, copying at both boundaries so a captured key cannot alias it.
fun vector4Target(name: String, get: () -> Vector4fc, set: (Vector4f) -> Unit) Register a vec4 local, copying at both boundaries so a captured key cannot alias it.
fun stringTarget(name: String, get: () -> String, set: (String) -> Unit) Register a string local. Strings retain the keyframer's endpoint-step interpolation.
fun include(contributor: KeyframeContributor, prefix: String = ""): KeyframeScope Add every lane supplied by contributor to this scope and return the scope for chaining.
fun target(module: ModulePointer, name: String) Register a key against one of a graphics module's live uniform structures, resolved through the pointer each time so it survives a shader reload.
fun target(module: ComputeModulePointer, name: String) target for a compute module's live uniform structures.
fun target(module: MeshModulePointer, name: String) target for a mesh-shader module's live uniform structures.
fun stackAuto(module: ModulePointer, name: String, keyName: String = name) Register a key against a stack.auto.<name> shader-source slot, so that the StackSL a module runs is part of the snapshot: capture reads it as a StackSLString and applying a key recompiles the slot. This is what lets a keyframe morph a patch's code as well as its numbers.
fun stackAuto(module: ComputeModulePointer, name: String, keyName: String = name) stackAuto for a compute module; the source is reloaded on the compute path.
fun stackAuto(module: MeshModulePointer, name: String, keyName: String = name) stackAuto for a mesh-shader module; the source is reloaded on the mesh path.
fun stackAutoNumbers(module: ModulePointer, slot: String, keyPrefix: String = slot) Register each number typed in the StackSL of a stack.auto.<slot> slot as a key of its own — slot.k0, slot.k1, … numbered as the box's numbers card numbers them — so a key snapshots them and applying or blending keys turns them on the GPU with no recompile (StackAutoNumbers).
fun stackAutoNumbers(module: ComputeModulePointer, slot: String, keyPrefix: String = slot) stackAutoNumbers for a compute module.
fun stackAutoNumbers(module: MeshModulePointer, slot: String, keyPrefix: String = slot) stackAutoNumbers for a mesh-shader module.
fun camera(camera: Camera, keyName: String = "camera") Register a Camera as a keyable target, so a viewpoint can be snapshotted and morphed like any other value. Capture stores the camera's CameraPose — its rigid transform plus focus distance — and applying a key calls applyPose, so interpolate gives you a screw-motion (sclerp) camera move rather than a separate position and target lerp.
fun findByName(name : String) : ((Function0<*>) -> Unit)? The raw "bind a lambda to this target" function for a registered key name, or null if there is no such key — an escape hatch for driving a target continuously instead of through a Key.
fun missingKey(name: String, value: Any?) Supply the value a load should use for a key that the file does not contain — how a scope that has grown a new parameter still loads snapshots taken before it existed.
fun missingKey(name: String, value: () -> Any?) missingKey with the default computed at load time rather than fixed now — use it when the sensible fallback is "whatever the target currently holds".
fun missingKey(schemaVersion: Int, name: String, value: Any?) A default that applies only when the loaded file's KeyDTO.schemaVersion equals schemaVersion — for the case where "the old files meant something different" rather than "the old files are missing this".
fun missingKey(schemaVersion: Int, name: String, value: () -> Any?) The schema-specific missingKey with a lambda default. Evaluated at most once per load, and only when the file actually lacks the name — a default whose name the file already carries is never called at all.
fun onChange(listener: (name: String) -> Unit) Call listener with the key name whenever a control declared with by keys.slider(...) (and the other KeyframeControl kinds) changes from outside the code: a hand on the control, or a recalled or morphed key. Assigning the local in code does not call it, and neither does a rerun: the local already holds the panel's last value from its declaration, so the body that follows sees it.
fun capture() Read every registered target right now into a new Key — the snapshot call.
fun interpolate(a: Key, b: Key, t: Double): Key Blend two snapshots into a new Key at position t, per value type: numbers and vectors lerp, quaternions slerp, dual quaternions and camera poses sclerp, and strings step to b's value once t >= 1.
fun encodeKeyJson(key: Key): String key as the same pretty JSON saveKey writes, without writing anything.
fun saveKey(path: Path, key: Key, note: String? = null) Write key to path as pretty JSON, creating parent directories — and, every time, also append a numbered copy to a <path>.versions directory with a manifest row carrying note and a timestamp. Saving is therefore always additive history, never just an overwrite; nothing prunes it, so a hundred saves leave a hundred files.
fun loadKey(path: Path, version: Int? = null): Key Read a saved key back as a Key, filling in any missingKey defaults for names the file lacks. Reading only: nothing is applied to the sketch until you invoke the returned key.
fun loadLatestKey(path: Path): Key The highest-numbered entry of path's version history, falling back to path itself when there is no history at all.
fun loadKeyVersion(path: Path, version: Int): Key loadKey(path, version) by another name, for when the version is not optional at the call site.
fun keyVersions(path: Path): List<KeyVersionInfo> The save history of path, oldest first — the list to show a person choosing which snapshot to go back to.
fun savedKeyNames(dir: Path): List<String> The names of the keys saved directly in dir — each file saveKey wrote there, sorted, without the .versions histories beside them. Empty when dir does not exist yet. This is the list a preset strip shows; KeyframeCollection.load reads the same files with their contents.
fun interpolateAlong(keys: List<Key>, t: Double): Key interpolate along a row of keys: they sit evenly along t in 0..1, in order, and between two neighbours the blend is theirs. t is clamped here, since past either end there is no neighbour to extrapolate towards. One key comes back as itself; none is an error.

KeyDiffKind

enum class KeyDiffKind

How one name differs between two keys, always read left-to-right: ADDED means the right key has it and the left does not, REMOVED the other way round, CHANGED that both have it with unequal values. There is no "same" kind — unchanged names are left out of a diff entirely.

Constants: ADDED, REMOVED, CHANGED.

KeyDiffEntry

data class KeyDiffEntry(val name: String, val kind: KeyDiffKind, val left: Any?, val right: Any?)

One line of a KeyDiff: what the name was on each side, and which way it moved.

Declaration Summary
val name: String The key name, as it appears in both snapshots (@-qualified if that is how it was registered).
val kind: KeyDiffKind Whether this name was added, removed, or changed between left and right.
val left: Any? The left-hand value; null for an ADDED entry, which had none.
val right: Any? The right-hand value; null for a REMOVED entry.

KeyDiff

data class KeyDiff(val leftName: String, val rightName: String, val entries: List<KeyDiffEntry>) : ToHTML

The difference between two KeyframeScope.Keys: only the names that actually moved, sorted by name, ready to render. Produced by Key.diff.

Declaration Summary
val leftName: String Heading for the left-hand column — cosmetic, defaulting to "left" at the call site.
val rightName: String Heading for the right-hand column.
val entries: List<KeyDiffEntry> The changes, name-sorted; empty means the two keys agree.
override fun toString(): String The diff as text, one line per change prefixed + (added), - (removed) or ~ (changed), and the single line "No key differences." when there are none.
override fun toHTML(): String The diff as a small dark-mode HTML table — one row per change, marker coloured green for added, red for removed and amber for changed, with a header counting the changes. What a box shows when a KeyDiff is the value on screen.

keyframes.kt

Keyframe curves edited by hand in a box's output panel and read back by code as a number at any time.

keyframes(name = "x") opens a curve editor in the box's output area and returns a KeyframeEvaluator; the curve the user draws is saved on the box under that name, so it survives a re-run and a reload. Seed a new curve with times, (time, value) pairs, or Keyframes carrying their segment types:

val x = keyframes(name = "x", defaults = listOf(Vec2(0.0, 0.0), Vec2(5.0, 1.0), Vec2(10.0, 0.0)))
val now = x.evaluateValueAt(2.5)

The evaluator also edits the curve from code (add, remove, edit, simplify, smooth), moves the editor's playhead with setTime, and draws reference overlays with setOverlay. Tests: KeyframeEvaluatorTest, KeyframerTest.

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

Used in: The keyframe editor: drawing a value over time, Agent transcript gallery

Top-level functions

Declaration Summary
fun loadKeyframeData(box: Box, name: String): String? Load persisted keyframe data for a given box and keyframer name. Returns the JSON string of keyframes, or null if none persisted.
fun saveKeyframeData(box: Box, name: String, keyframesJson: String) Save keyframe data for a given box and keyframer name. Merges with any existing keyframeData on the box.
fun installKeyframeHandler()
fun Box.keyframes(channel: String? = null, name: String, defaultTimes: List<Number>? = null): KeyframeEvaluator Creates (or reconnects to) a keyframe curve editor displayed in the given output channel.
fun Box.keyframes(channel: String? = null, name: String, defaults: List<Vec2>): KeyframeEvaluator Creates (or reconnects to) a keyframe curve editor, with default keyframes specified as Vec2 pairs of (time, value).
fun Box.keyframes(channel: String? = null, name: String, keys: List<Keyframe>): KeyframeEvaluator Creates (or reconnects to) a keyframe curve editor, seeded with keys — keys with their types, so a curve can start with a held step or an eased segment rather than straight lines.

Top-level values

Declaration Summary
val knownKeyframeTargets

KeyframeEvaluator

class KeyframeEvaluator(jsonString: String)

KeyframeEvaluator - Evaluates animation curves defined by keyframes

Declaration Summary
lateinit var session: WebSocketSession
val hasLiveSession: Boolean True when the iframe WebSocket is connected and still open.
var associatedBoxId: String? The box ID + name uid this evaluator is associated with
var associatedName: String?
fun isEmpty(): Boolean
fun set(jsonString: String) Replaces every keyframe with those in jsonString, a JSON list in the same form the constructor takes. They are sorted by time; fields this class does not know are ignored.
fun setTime(t: Double) Moves the editor's playhead to t: an orange line, and a dot where it crosses the curve.
fun send() Sends the current keys to the editor, with this box's custom key types and their segments.
fun evaluateValueAt(time: Double): Double Evaluates the animation curve value at a given time
fun getTimeRange(): Pair<Double, Double> Returns the time range over which the keyframes are defined
fun getKeyframes(): List<Keyframe> Returns a defensive copy of the current keyframe list (sorted by time).
fun getKeyframeCount(): Int Number of keyframes currently defined.
fun getKeyframeAt(index: Int): Keyframe Returns the keyframe at the given index (0-based, sorted by time).
fun getKeyframeTimes(): List<Double> Returns times of all keyframes (sorted ascending).
fun getKeyframeValues(): List<Double> Returns values of all keyframes (in time order).
fun getKeyframeTypes(): List<String> Returns the interpolation types of all keyframes (in time order).
fun getKeyframePoints(): List<Vec2> Returns keyframes as a list of Vec2(time, value).
var onChange: ((List<Keyframe>) -> Fn?)? Callback invoked whenever the keyframe data changes (e.g. user edits in the UI). Receives the updated list of Keyframes. If it returns an Fn, that function replaces the editor's overlay (the one setOverlay draws with its default id). Return null to leave the overlay unchanged, or clear it explicitly with clearOverlay.
fun evaluateDerivativeAt(time: Double, h: Double = 0.0): Double The rate of change of the curve at time, in value per unit of time: exact, from the shape of the segment time falls in. Zero outside the keys and along a hold. At a key it is the slope of the segment leaving it (arriving, at the last key).
fun setKeys(keys: List<Keyframe>) Replaces the keys with keys, as an edit would: the editor shows them and they are saved with the box. A bezier key without handles gets the ones the editor derives from its neighbours. onChange is not called; it is for edits made in the editor.
fun add(time: Double, value: Double, type: String = "linear"): Keyframe Adds a key at time with value and type; see setKeys. Returns the new key.
fun remove(key: Keyframe) Removes key (compared by value); see setKeys.
fun removeAt(index: Int) Removes the key at index, in time order; see setKeys.
fun clear() Removes every key; see setKeys.
fun edit(block: MutableList<Keyframe>.() -> Unit) Edits the keys as a list, then saves and shows the result; see setKeys.
fun simplify(tolerance: Double) Removes every key it can while the curve stays within tolerance of where it is now, in the curve's own units, so a dense take becomes a few smooth keys. Keys that stay become bezierContinuous, with handles that follow the curve's slope; locked, hold and custom keys stay as they are. The editor's Simplify slider does the same. See setKeys.
fun smooth(width: Double) Smooths the keys' values with a Gaussian width wide in time (its standard deviation), so the jitter in a recorded take goes and its shape stays. The first and last keys and locked keys keep their values, and no key moves in time. The editor's Smooth slider does the same. See setKeys.
fun setOverlay(samples: List<Vec2>, color: String = "#ff9800", strokeWidth: Double = 1.5, dashArray: String? = "6 3", id: String = "default") Send an overlay curve to the keyframe editor UI. The curve is drawn on top of the main keyframe graph as a non-interactive visual reference.
fun setOverlay(fn: Fn, sampleCount: Int = 200, color: String = "#ff9800", strokeWidth: Double = 1.5, dashArray: String? = "6 3", id: String = "default") Draw an overlay curve from a Fn, sampled over the keyframe time range.
fun clearOverlay(id: String = "default") Remove a specific overlay curve from the editor.
fun clearAllOverlays() Remove all overlay curves from the editor.

Keyframe

data class Keyframe(val time: Double, val value: Double, val type: String, val handles: Handles? = null, val locked: Boolean = false)

Declaration Summary
val time: Double
val value: Double
val type: String
val handles: Handles?
val locked: Boolean

Handles

data class Handles(val `in`: Handle, val out: Handle)

Declaration Summary
val `in`: Handle
val out: Handle

Handle

data class Handle(val time: Double = 0.0, val value: Double = 0.0)

Declaration Summary
val time: Double
val value: Double

ArdySmoke.kt

src/jvmMain/kotlin/afield/animation/mocap/ArdySmoke.kt — read the source

ArdySmoke

object ArdySmoke

Actual model run, never substituted with a fixture. Cached prompt embeddings can run without the encoder service.

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

CLoSDSmoke.kt

src/jvmMain/kotlin/afield/animation/mocap/CLoSDSmoke.kt — read the source

CLoSDSmoke

object CLoSDSmoke

Real Kotlin process/skeleton/reference/executed/intervention check, explicitly skipped without its opt-in home.

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

MotionInteropSmoke.kt

src/jvmMain/kotlin/afield/animation/mocap/MotionInteropSmoke.kt — read the source

MotionInteropSmoke

object MotionInteropSmoke

Real Kotlin -> Python -> Kotlin binary round trip; no network inference is claimed.

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

PatchyWeb.kt

Drive the browser build of the Patchy editor from box code: Box.patchyWeb shows Patchy in an output channel and returns a PatchyWebSession.

The session's suspend methods (PatchyWebSession.state, PatchyWebSession.script, PatchyWebSession.newDocument, PatchyWebSession.addLayer, PatchyWebSession.fillRect, PatchyWebSession.undo, PatchyWebSession.redo) send tool calls to the Patchy page and wait for its replies, so call them inside go { ... }.

src/jvmMain/kotlin/afield/app/server/PatchyWeb.kt — read the source

Used in: Patchy

Top-level functions

Declaration Summary
fun Box.patchyWeb(channel: String = "patchy", width: Int = 1200, height: Int = 800, documentFile: File? = null, autosave: Boolean = documentFile != null): PatchyWebSession Show the Field build of Patchy, with its visible canvas controlled by this box's Kotlin.

PatchyWebSession

class PatchyWebSession internal constructor() : AutoCloseable

One live browser Patchy session. Call suspend methods inside go { ... }.

Declaration Summary
var connected
suspend fun state(): JsonObject Read live documents, layer IDs and the edit token.
suspend fun script(code: String): JsonElement? Run Patchy's documented JavaScript API in the visible editor.
suspend fun newDocument(width: Int, height: Int): String Create a document in the visible editor and return its stable ID.
suspend fun addLayer(documentId: String, name: String): String Add a paint layer to an open document.
suspend fun fillRect(documentId: String, layerId: String, x: Int, y: Int, width: Int, height: Int, color: String) Paint a solid rectangle through Patchy's layer API.
suspend fun undo(documentId: String): Boolean Undo the last edit in a live document.
suspend fun redo(documentId: String): Boolean Redo the last undone edit in a live document.
override fun close()

fline_graph_connectivity.kt

Bridge from drawn geometry to the graph algorithms: turns one or more FLines into a welded point graph.

flineGraph, or the toFLineGraph extensions on FLine and Iterable<FLine>, adaptively subdivides cubics down to cubicFlatness, samples every node, then welds samples closer than epsilon using a uniform grid plus union-find (epsilon = 0 welds only bit-identical positions). A welded vertex is the mean of the samples that reached it, and FLineGraph.vertexSources records which line index and node instruction each of those samples came from. Edges join consecutive samples, close() joins back to the subpath start, self-edges are dropped and duplicates collapse to the shortest.

The result carries a PointGraph of GraphEdges — the same types as graph_connectivity.kt — so the search, spanning-tree and analysis functions there apply directly to a drawing.

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

Top-level functions

Declaration Summary
fun flineGraph(lines: Iterable<FLine>, epsilon: Number = 1e-4f, cubicFlatness: Number = epsilon, maxCubicSubdivisions: Int = 12): FLineGraph Linearize lines into a graph by adaptively flattening curves and welding vertices whose distance is <= epsilon.
fun Iterable<FLine>.toFLineGraph(epsilon: Number = 1e-4f, cubicFlatness: Number = epsilon, maxCubicSubdivisions: Int = 12): FLineGraph Convenience extension for flineGraph.
fun FLine.toFLineGraph(epsilon: Number = 1e-4f, cubicFlatness: Number = epsilon, maxCubicSubdivisions: Int = 12): FLineGraph Convenience extension for a single FLine.

FLineGraphSource

data class FLineGraphSource(val lineIndex: Int, val instruction: Float, val position: Vec3)

A sampled source point that contributed to a welded FLineGraph vertex.

Declaration Summary
val lineIndex: Int
val instruction: Float
val position: Vec3

FLineGraph

data class FLineGraph(val vertices: List<Vec3>, val graph: PointGraph, val vertexSources: List<List<FLineGraphSource>>)

Graph view of one or more FLines.

Declaration Summary
val vertices: List<Vec3>
val graph: PointGraph
val vertexSources: List<List<FLineGraphSource>>
val edges: List<GraphEdge>
val pointCount: Int

graph_connectivity.kt

Self-contained graph / computational-geometry library over 2D and 3D point sets (Vec2/Vec3). Every algorithm has a vec2* and (where meaningful) a vec3* entry point returning a PointGraph; see each function's own KDoc for its definition and complexity.

Proximity / neighbourhood families: KNN (vec2KnnGraph), Orthant (vec2OrthantGraph), Relative Neighborhood (vec2RelativeNeighborhoodGraph), Gabriel (vec2GabrielGraph), Beta-Skeleton (vec2BetaSkeleton), Nearest-Neighbor (vec2NearestNeighborGraph), Minimum Spanning Tree (vec2MinimumSpanningTree), Maximum-Diameter/radius (vec2MaximumDiameterGraph), Maximum-Cut (vec2MaximumCutGraph) and Complete (vec2CompleteGraph) graphs.

JTS-backed (2D only): full Delaunay triangulation (vec2DelaunayGraph, vec2DelaunayTriangles), Urquhart (vec2UrquhartGraph, a real Urquhart graph built by removing the longest edge of each Delaunay triangle; there is no 3D one, because Field has no 3D Delaunay of a point set), Voronoi diagram/dual (vec2VoronoiDiagram, vec2VoronoiDualGraph) and convex hull (vec2ConvexHull, vec2ConvexHullGraph, vec2HullProjectionGraph).

Traversal / analysis: BFS/DFS/A* path search, GraphEdgeVisitState greedy walks and graph-diameter path decomposition (see the search/path functions below), plus FLine rendering integration in fline_graph_connectivity.kt.

Custom distance metrics: pass GraphConnectivityOptions.customDistanceMetric. It is honoured by the distance-driven algorithms (KNN, MST, RNG, Gabriel, Beta-Skeleton, Delaunay/Urquhart edge weights, etc.). This includes vec2OrthantGraph/vec3OrthantGraph, whose nearest-neighbour ranking within each orthant honours customDistanceMetric when supplied (Euclidean otherwise).

Smoke tests: afield.examples.stdlib.GraphConnectivitySmokeTest (its runTests() drives 35 checks, covering path search, edge-visit-state and FLine rendering as well as the core graphs). Live rendered examples: public/docs/graph_connectivity_examples.md.

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

Used in: Neighbours and graphs on a point cloud, Points into structure — Voronoi, Apollonius, contours and clusters

Top-level functions

Declaration Summary
fun pointGraphEdgeVisitState(graph: PointGraph, initiallyVisited: Collection<GraphEdge> = emptyList()): GraphEdgeVisitState Build a GraphEdgeVisitState from a PointGraph.
fun PointGraph.edgeVisitState(initiallyVisited: Collection<GraphEdge> = emptyList()): GraphEdgeVisitState Convenience extension for pointGraphEdgeVisitState.
fun graphBfsPath(graph: PointGraph, start: Int, goal: Int, allowedEdges: Set<GraphEdge>? = null): GraphPath? Find an unweighted shortest path from start to goal using breadth-first search.
fun graphDfsPath(graph: PointGraph, start: Int, goal: Int, allowedEdges: Set<GraphEdge>? = null): GraphPath? Find a depth-first path from start to goal.
fun graphAStarPath(graph: PointGraph, start: Int, goal: Int, heuristic: (Int, Int) -> Double = { _, _ -> 0.0 }, allowedEdges: Set<GraphEdge>? = null): GraphPath? Find a weighted path from start to goal using A*.
fun vec2AStarPath(graph: PointGraph, points: List<Vec2>, start: Int, goal: Int, allowedEdges: Set<GraphEdge>? = null): GraphPath? A* path helper using Euclidean distance on 2D points as the heuristic.
fun vec3AStarPath(graph: PointGraph, points: List<Vec3>, start: Int, goal: Int, allowedEdges: Set<GraphEdge>? = null): GraphPath? A* path helper using Euclidean distance on 3D points as the heuristic.
fun graphGreedyClosestUnvisitedPaths(graph: PointGraph, anchor: Int, edgeState: GraphEdgeVisitState = graph.edgeVisitState()): GraphPathSetResult Build a set of paths from anchor by repeatedly choosing the closest reachable point that is incident to at least one unvisited edge.
fun graphDiameterPath(graph: PointGraph, allowedEdges: Set<GraphEdge>? = null): GraphPath? Compute a weighted graph diameter path (the shortest path between the farthest pair of nodes).
fun graphDiameterPathDecomposition(graph: PointGraph, edgeState: GraphEdgeVisitState = graph.edgeVisitState()): GraphPathSetResult Iteratively removes diameter paths from the unvisited edge set until all reachable edges are consumed, producing a long-to-short path decomposition.
fun graphAnchoredGreedyThenDiameterCover(graph: PointGraph, anchor: Int, edgeState: GraphEdgeVisitState = graph.edgeVisitState()): GraphPathSetResult Build an anchored cover by combining: 1) Greedy closest-unvisited anchored paths 2) Diameter decomposition for any disconnected leftovers
fun vec2KnnGraph(points: List<Vec2>, k: Int, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a K-Nearest Neighbors graph for 2D points.
fun vec3KnnGraph(points: List<Vec3>, k: Int, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a K-Nearest Neighbors graph for 3D points.
fun vec2OrthantGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute an Orthant Neighborhood Graph for 2D points.
fun vec3OrthantGraph(points: List<Vec3>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute an Orthant Neighborhood Graph for 3D points.
fun vec2RelativeNeighborhoodGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Relative Neighborhood Graph (RNG) for 2D points.
fun vec3RelativeNeighborhoodGraph(points: List<Vec3>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Relative Neighborhood Graph (RNG) for 3D points.
fun vec2GabrielGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Gabriel Graph for 2D points.
fun vec3GabrielGraph(points: List<Vec3>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Gabriel Graph for 3D points.
fun vec2BetaSkeleton(points: List<Vec2>, beta: Double = 1.0, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Beta-Skeleton for 2D points.
fun vec3BetaSkeleton(points: List<Vec3>, beta: Double = 1.0, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Beta-Skeleton for 3D points.
fun vec2MinimumSpanningTree(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a KNN-constrained minimum spanning tree for 2D points.
fun vec3MinimumSpanningTree(points: List<Vec3>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a KNN-constrained minimum spanning tree for 3D points.
fun vec2MaximumDiameterGraph(points: List<Vec2>, maxDiameter: Double, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Maximum Diameter Graph for 2D points.
fun vec3MaximumDiameterGraph(points: List<Vec3>, maxDiameter: Double, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Maximum Diameter Graph for 3D points.
fun vec2NearestNeighborGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Nearest Neighbor Graph (1-NN) for 2D points.
fun vec3NearestNeighborGraph(points: List<Vec3>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Nearest Neighbor Graph (1-NN) for 3D points.
fun vec2UrquhartGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute an Urquhart Graph for 2D points.
fun vec3UrquhartGraph(points: List<Vec3>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Not an Urquhart graph: a 3D one would remove the longest edges of a 3D Delaunay tetrahedralization of the points, and Field has no 3D Delaunay of a point set, so this returned the minimum spanning tree under the wrong name. Deprecated at ERROR level so a call says so at compile time; use vec3MinimumSpanningTree for what it computed.
fun vec2DelaunayGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a full Delaunay triangulation for 2D points using JTS.
fun vec2VoronoiDiagram(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): VoronoiDiagram Compute a Voronoi diagram for 2D points using JTS.
fun vec2VoronoiDualGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute the dual graph of a Voronoi diagram (equivalent to Delaunay triangulation).
fun vec2MaximumCutGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Maximum Cut approximation graph for 2D points.
fun vec3MaximumCutGraph(points: List<Vec3>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Maximum Cut approximation graph for 3D points.
fun vec2CompleteGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Complete Euclidean Graph for 2D points.
fun vec3CompleteGraph(points: List<Vec3>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a Complete Euclidean Graph for 3D points.
fun vec2ConvexHull(points: List<Vec2>): ConvexHull2D Compute the convex hull of 2D points using JTS.
fun vec2ConvexHullGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute a graph connecting convex hull points in order.
fun vec2DelaunayTriangles(points: List<Vec2>): List<Triple<Int, Int, Int>> Compute triangles from Delaunay triangulation.
fun vec2HullProjectionGraph(points: List<Vec2>, options: GraphConnectivityOptions = GraphConnectivityOptions()): PointGraph Compute the nearest point on the convex hull for each interior point.

GraphEdge

data class GraphEdge(val i: Int, val j: Int, val weight: Double)

Represents an edge between two points in a graph.

Declaration Summary
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

PointGraph

data class PointGraph(val edges: List<GraphEdge>, val pointCount: Int)

Represents a graph computed from a set of points.

Declaration Summary
fun neighbors(pointIndex: Int): List<Int> Get all neighbors of a point.
fun neighbors1(pointIndex: Int): List<Int>
fun degree(pointIndex: Int): Int Get the degree (number of neighbors) of a point.

GraphConnectivityOptions

data class GraphConnectivityOptions(val allowDuplicatePoints: Boolean = true, val customDistanceMetric: ((Int, Int, List<*>) -> Double)? = null)

Options for graph connectivity algorithms.

Declaration Summary
val allowDuplicatePoints: Boolean
val customDistanceMetric: ((Int, Int, List<*>) -> Double)?

GraphPath

data class GraphPath(val nodes: List<Int>, val edges: List<GraphEdge>, val totalWeight: Double)

Represents a path through a PointGraph.

Declaration Summary
val nodes: List<Int>
val edges: List<GraphEdge>
val totalWeight: Double
val start: Int
val end: Int

GraphEdgeVisitState

data class GraphEdgeVisitState(val pointCount: Int, val visitedEdges: Set<GraphEdge>, val unvisitedEdges: Set<GraphEdge>)

Tracks visited and unvisited edges for path cover generation.

Declaration Summary
val pointCount: Int
val visitedEdges: Set<GraphEdge>
val unvisitedEdges: Set<GraphEdge>
val allEdges: Set<GraphEdge>
fun isVisited(edge: GraphEdge): Boolean
fun isUnvisited(edge: GraphEdge): Boolean
fun markVisited(edges: Iterable<GraphEdge>): GraphEdgeVisitState
fun markVisited(edge: GraphEdge): GraphEdgeVisitState
fun markUnvisited(edges: Iterable<GraphEdge>): GraphEdgeVisitState
fun markUnvisited(edge: GraphEdge): GraphEdgeVisitState
fun visitedGraph(): PointGraph
fun unvisitedGraph(): PointGraph

GraphPathSetResult

data class GraphPathSetResult(val paths: List<GraphPath>, val edgeState: GraphEdgeVisitState)

Result containing a list of paths and the updated edge-visit state.

Declaration Summary
val paths: List<GraphPath>
val edgeState: GraphEdgeVisitState

VoronoiCell

data class VoronoiCell(val siteIndex: Int, val vertices: List<Vec2>)

Represents a Voronoi cell in the Voronoi diagram.

Declaration Summary
val siteIndex: Int
val vertices: List<Vec2>

VoronoiDiagram

data class VoronoiDiagram(val cells: List<VoronoiCell>, val pointCount: Int)

Represents a Voronoi diagram computed from a set of points.

Declaration Summary
val cells: List<VoronoiCell>
val pointCount: Int
fun cellForSite(siteIndex: Int): VoronoiCell? Get the Voronoi cell for a specific site point.

ConvexHull2D

data class ConvexHull2D(val hullPoints: List<Vec2>, val hullIndices: List<Int>)

Represents a convex hull of a point set.

Declaration Summary
val hullPoints: List<Vec2>
val hullIndices: List<Int>

PrincipalCurves.kt

Elastic principal graphs: smooth curves, trees and forests fitted through a 2-D or 3-D point cloud, as a port of ElPiGraph.

PrincipalCurves is the entry point, and what it hands back is not a finished answer but an optimisation state. A PrincipalForest can be fitted again, grown with more nodes under one of the graph grammars Growth offers, have nodes moved and pinned, and be read back at any point along the way: as node positions and edges, as FLines, or as a GeometryNetwork whose channels carry the per-node measurements. So pinning a node, replacing the points under a fitted graph, or adding nodes to it continues from where the fit had reached rather than starting over.

ElasticParameters holds the energy the fit minimises, ElasticEnergy its value split by term, PointAssignments which node and edge each point ended up on, and NodeStatistics the spread of the points around each node, which is what the ribbon widths are drawn from.

The solver is the optional native library field_principal_curves (PrincipalCurvesLibrary), so a forest holds native memory, is closed by the box's next run, and PrincipalCurves.isAvailable reports whether the library loaded at all. The worked account is public/docs/principal_curves.md.

src/jvmMain/kotlin/afield/geometry/curves/principal/PrincipalCurves.kt — read the source

Used in: Principal curves: smooth curves through point clouds, Chaining the geometry extensions

ElasticParameters

data class ElasticParameters(val lambda: Double = 0.01, val mu: Double = 0.1, val trimmingRadius: Double = 0.0, val branchPenalty: Double = 0.0, val maxIterations: Int = 30, val eps: Double = 1e-3, val candidateSample: Int = 5000, val threads: Int = 0, val seed: Long = 1)

The weights of the elastic energy and the solver's controls.

Declaration Summary
val lambda: Double Penalises stretching — the squared length of every edge, and so total length.
val mu: Double Penalises bending: how far each node of degree two or more sits from the mean of its neighbours.
val trimmingRadius: Double Makes the fit robust: points farther than this from every node stop pulling. 0 turns it off.
val branchPenalty: Double Added per unit of excess degree at branch points when choosing how to grow, so a branch must reduce the energy by more than this to be kept.
val maxIterations: Int EM iterations per fit.
val eps: Double Convergence threshold, as a relative change of the node positions.
val candidateSample: Int How many points are sampled to score growth candidates; 0 scores against all of them.
val threads: Int Worker threads; 0 uses the hardware concurrency.
val seed: Long Seed for the candidate sampling and the k-means initialisation.

Growth

enum class Growth(internal val flags: Int)

Which graph-grammar operations growth may use.

Constants: CURVES, TREES, REFINE.

ElasticEnergy

data class ElasticEnergy(val total: Double, val approximation: Double, val stretching: Double, val bending: Double, val trimmedFraction: Double)

The elastic energy and its parts; trimmedFraction is the share of point weight beyond the trimming radius.

PointAssignments

class PointAssignments(val node: IntArray, val edge: IntArray, val t: DoubleArray, val distance: DoubleArray)

Where each point went: its nearest node, the nearest edge at that node (-1 if trimmed or if the node has no edges), the parameter t along that edge from its first node, and its distance to the graph.

NodeStatistics

class NodeStatistics(val dimension: Int, val support: DoubleArray, val nEff: DoubleArray, val residual: DoubleArray, val spread: DoubleArray, val standardError: DoubleArray, val tangent: DoubleArray, val basis: DoubleArray)

Per-node statistics of the points projected onto the graph. Each point is shared between the two ends of its edge by its parameter.

Declaration Summary
val dimension: Int
val support: DoubleArray
val nEff: DoubleArray
val residual: DoubleArray
val spread: DoubleArray
val standardError: DoubleArray
val tangent: DoubleArray
val basis: DoubleArray
val width: DoubleArray

PrincipalForest

class PrincipalForest internal constructor(val dimension: Int, points: DoubleArray, handle: MemorySegment, parameters: ElasticParameters) : AutoCloseable

An elastic principal graph fitted to a point cloud, kept as an optimisation state: refine it, grow it, move or pin nodes, split edges, and read it back at any point.

Declaration Summary
val dimension: Int
val pointCount: Int
fun setPoints(points: DoubleArray, weights: DoubleArray? = null) Replaces the points (and weights), keeping the graph, so that refine follows data that moves: a particle system, a stipple that changes, a camera feed's bright pixels.
var parameters: ElasticParameters
fun initComponents(components: Int = 1, nodesEach: Int = 3) Replaces the graph with components separate chains of nodesEach nodes. The points are clustered by single linkage (k-means centres joined by their minimum spanning tree, cut at its longest edges), and each chain starts along the longest path through its cluster, so curved strands are followed rather than cut across.
fun initTree(k: Int) Replaces the graph with the minimum spanning tree of k k-means centres.
fun initGraph(nodePositions: DoubleArray, edges: IntArray) Replaces the graph with one given as node positions (dimension per node) and edges (pairs of node indices).
fun initFromFLines(lines: List<FLine>, nodesPerCurve: Int = 8) Replaces the graph with lines: each subpath becomes a chain of nodesPerCurve nodes evenly spaced along it, closed subpaths become loops, and separate subpaths stay separate. Draw a few strokes, then fit or grow them onto the points.
fun fit(iterations: Int = 0): Int Fits node positions to the points without changing the topology. Returns the iterations used.
fun refine(iterations: Int = 5): Int fit with a small iteration budget, for refining a little at a time (from a frame loop, say).
fun grow(nodes: Int, growth: Growth = Growth.CURVES): Int Grows the graph by grammar operations until it has nodes nodes, choosing at each step the operation whose fitted configuration has the lowest energy. Returns the steps applied.
fun moveNode(node: Int, x: Double, y: Double, z: Double = 0.0, fixed: Boolean = true) Moves node to (x, y, z) and pins it there (unless fixed is false). Call fit to settle the rest.
fun fixNode(node: Int, fixed: Boolean = true) Pins or frees node where it is.
fun splitEdge(edge: Int, t: Double = 0.5): Int Inserts a node on edge at parameter t (0..1 from its first node). Returns the new node's index.
val nodePositions: DoubleArray Node positions, dimension doubles per node, in node-index order.
val edges: IntArray Edges as pairs of node indices.
val nodeCount: Int
fun energy(): ElasticEnergy
fun assignments(): PointAssignments
fun nodeStatistics(): NodeStatistics
val network: GeometryNetwork The graph as a GeometryNetwork. Nodes are the graph's branch points and endpoints (node index as id); curves are the chains between them, so a curve's interior vertices are graph nodes of degree two. A component with no endpoints is one closed curve. Per-vertex samples: width, spread, support, nEff, residual, standardError, graphNode, and in 3D covariance (uu, uv, vv). Node channels: degree, fixed.
fun toFLines(smooth: Boolean = true, color: Vec4 = Vec4(0.9f, 0.3f, 0.1f, 1f), thickness: Double = 2.0): List<FLine> The fitted curves as FLines (splines through the nodes with smooth), each carrying geometryNetwork.
fun widthFLines(scale: Double = 2.0, color: Vec4 = Vec4(0.95f, 0.55f, 0.2f, 0.3f)): List<FLine> 2D only: a filled ribbon around each curve, scale standard deviations of the transverse spread to each side, so its width shows how spread out the points are along the curve.
fun assignmentFLines(every: Int = 1, color: Vec4 = Vec4(0.3f, 0.3f, 0.3f, 0.35f)): FLine Short lines from every everyth point to where it projects onto the graph, coloured by edge.
fun pointsFLine(pointSize: Double = 2.0, color: Vec4 = Vec4(0.2f, 0.4f, 0.8f, 0.6f)): FLine The input points as one FLine of dots, for display.
val isClosed: Boolean
override fun close()
override fun toString()

PrincipalCurves

object PrincipalCurves

Principal curves, trees and forests: smooth curves fitted through weighted 2D or 3D point clouds, as elastic principal graphs (a port of ElPiGraph). Needs the optional native library field_principal_curves (see PrincipalCurvesLibrary).

Declaration Summary
val isAvailable: Boolean
fun create(points: DoubleArray, dimension: Int = 2, weights: DoubleArray? = null, parameters: ElasticParameters = ElasticParameters()): PrincipalForest A forest over points (dimension doubles per point) with no graph yet; initialise it, then fit or grow.
fun create(points: List<afield.math.linalg.Vec2>, weights: DoubleArray? = null, parameters: ElasticParameters = ElasticParameters()): PrincipalForest A forest over 2D points.
fun fit(points: DoubleArray, dimension: Int = 2, weights: DoubleArray? = null, curves: Int = 1, nodes: Int = 30, allowBranching: Boolean = false, parameters: ElasticParameters = ElasticParameters(), from: List<FLine>? = null): PrincipalForest Fits curves curves (separate components, one per k-means cluster to start with) with nodes nodes in all. With allowBranching the components may grow into trees. With from, starts from those strokes instead, keeping their components and closed loops.
fun fitAsync(points: DoubleArray, dimension: Int = 2, weights: DoubleArray? = null, curves: Int = 1, nodes: Int = 30, allowBranching: Boolean = false, parameters: ElasticParameters = ElasticParameters()): CompletableFuture<PrincipalForest> fit on a worker thread, for runAsync. A forest made off the box's thread is yours to close.

PrincipalCurvesNative.kt

Finding field_principal_curves on this machine, and calling into it once found.

PrincipalCurvesLibrary is the half that answers without loading anything: the library's file name on this OS, the paths it is looked for in, whether one of them exists, and the Gradle task and per-OS shell commands that build it. NativeGeometry registers it as a NativeGeometryCapability, so the welcome page's Geometry extensions panel can report on the library whether or not it is installed. The path is resolved afresh on every read, so a library built during a live session is picked up by the next box run.

The other half, internal to the package, binds the C ABI declared in native/field-principal-curves/src/field_principal_curves.h with java.lang.foreign: one lazy downcall handle per fpc_ entry point, the solver's parameter struct laid out by hand with its padding, and a 512-byte buffer that each call which can fail carries down and reads a message out of when it reports failure. Reading the fit back out (the node and edge arrays, the energies, the per-point assignments, the per-node statistics) takes no error buffer. The first symbol lookup checks fpc_abi_version against the version this binding expects and refuses a library that disagrees, naming the task that rebuilds it.

PrincipalCurves is the API over it, and its only caller.

src/jvmMain/kotlin/afield/geometry/curves/principal/PrincipalCurvesNative.kt — read the source

PrincipalCurvesLibrary

object PrincipalCurvesLibrary : NativeGeometryCapability

Where field_principal_curves is, and whether it is there. Field runs without it; describe says how to build it.

Declaration Summary
override val name
override val title
override val docsPage
override val buildTask
override val install: Map<String, List<String>>
val fileName: String
val searchedPaths: List<Path> Every path resolvedPath tries, in order.
val resolvedPath: Path? Not cached: a library built during a live session is picked up by the next box run.
override val isAvailable: Boolean
val HOW_TO_BUILD: String
override fun describe(): String

Elements.kt

Propositions from Euclid's Elements, constructed with compass and straightedge in the manner of Oliver Byrne's coloured edition (1847).

Each proposition takes geometry in and returns a Construction: its Construction.result (the triangle, segment, line or circle the proposition asks for) and the numbered Construction.steps that build it, each holding the coloured circles, lines and lettered points it draws. A proposition that cites an earlier one carries that one's steps inside its own, prefixed with the citation, just as the text says "by I.1".

val c = Elements.prop1(Segment2(point2(100, 200), point2(220, 200)))
val triangle = c.result
lines(c.toFLines(width = 520.0, height = 400.0), w = 520.0, h = 400.0)  // the plate, fitted to the output

toFLines(upTo = 2) draws only the first two steps, for stepping through a proof, and toFLines(style = ByrneStyle.WORKING) shows the working of the propositions this one cites.

Every step is built only from the intersections of circles and straight lines in Euclid.kt; nothing is placed by a formula the proposition does not use. Where Euclid says "take any point", the choice here is fixed, so the same input always draws the same figure; Elements.solutions enumerates the others.

src/jvmMain/kotlin/afield/geometry/euclid/Elements.kt — read the source

Used in: Euclid's Elements

Top-level functions

Declaration Summary
fun <T> construction(proposition: String, enunciation: String, build: Proof.() -> T): Construction<T> Writes a proposition of your own, the way every proposition in Elements is written: build draws it step by step with Proof.step and returns its result.
fun <T> List<Construction<T>>.nearest(p: Point2): Construction<T> The solution whose result lies nearest p.
fun <T> List<Construction<T>>.farthest(p: Point2): Construction<T> The solution whose result lies farthest from p.

Byrne

enum class Byrne(val r: Double, val g: Double, val b: Double)

Byrne's palette, plus black.

Constants: RED, YELLOW, BLUE, BLACK.

Role

enum class Role

What a drawn element is for. A proposition says only this; ByrneStyle decides how each role looks, so every diagram is styled in one place.

Constants: GIVEN, CONSTRUCTION, RESULT, AREA.

Figure

data class Figure(val shape: Shape2, val color: Byrne, val label: String? = null, val role: Role = Role.GIVEN)

One drawn element of a construction: a shape in color playing role, perhaps lettered with label.

Pen

data class Pen(val width: Double, val dash: Double = 0.0, val alpha: Double = 1.0)

How one role is stroked: width in multiples of ByrneStyle.thickness, dashed when dash is above zero (dash and gap both that many thicknesses long), and faded by alpha.

ByrneStyle

data class ByrneStyle(val thickness: Double? = null, val given: Pen = Pen(1.0), val result: Pen = Pen(1.6), val circles: Pen = Pen(0.6), val constructionLines: Pen = Pen(0.6, dash = 2.0), val cited: Pen? = null, val areas: Pen = Pen(0.0, alpha = 0.8), val pointSize: Double = 0.8, val labels: Boolean = true, val labelSize: Double = 7.2, val labelDistance: Double = 2.76, val paper: Triple<Double, Double, Double>? = Triple(0.96, 0.93, 0.85))

The look of a Construction diagram.

Declaration Summary
val thickness: Double? Base stroke width; null, the default, takes it from the size of the figure, or of the output.
val given: Pen Pen for what the proposition is handed, and for the lines its proof keeps.
val result: Pen Pen for what the proposition was asked to make.
val circles: Pen Pen for the compass circles and arcs a construction draws.
val constructionLines: Pen Pen for the straight lines a construction draws: its joins and productions.
val cited: Pen? Pen for the steps of a cited proposition; null, the default, drops them, so a citation shows only what it produced. WORKING sets it, faded behind the main figure.
val areas: Pen Fill for coloured areas; only its Pen.alpha is read, since an area is filled rather than stroked.
val pointSize: Double The dot drawn at a lettered point.
val labels: Boolean Whether a point's letter is drawn beside it.
val labelSize: Double Letter height.
val labelDistance: Double How far up and to the right of its point a letter sits.
val paper: Triple<Double, Double, Double>? The sheet behind the figure, as red, green and blue, and the colour of the halo behind each letter; null for neither.

Step

data class Step(val text: String, val figures: List<Figure>, val cited: String? = null)

One sentence of a proof and what it draws; cited names the earlier proposition it comes from, if any.

Triangle2

data class Triangle2(val a: Point2, val b: Point2, val c: Point2)

A triangle by its three corners.

Quadrilateral2

data class Quadrilateral2(val a: Point2, val b: Point2, val c: Point2, val d: Point2)

A four-sided figure by its corners in order.

Construction

class Construction<out T>(val proposition: String, val enunciation: String, val result: T, val steps: List<Step>)

The result of a proposition and the steps that construct it.

Declaration Summary
val location: Point2 Where the result is: the mean of its points (a line's foot, a circle's centre).
fun toFLines(upTo: Int = steps.size, style: ByrneStyle = ByrneStyle.PLATE, extent: Double = 400.0, width: Double? = null, height: Double? = null): List<FLine> The diagram as coloured FLines through step upTo (all of them by default), drawn in style. Lines and rays are drawn extent long.
fun markdown(cited: Boolean = false) The enunciation and the numbered proof, as Markdown; cited steps are included when cited is true.
override fun toString()

Proof

class Proof internal constructor()

What a proposition is written with inside construction: numbered steps, the figures they draw, the choices it makes (pick, pickSide), the claim it checks (check) and the propositions it cites (by), plus the small measures proofs keep needing.

Declaration Summary
fun Shape2.drawn(color: Byrne, role: Role = Role.GIVEN) A figure: this shape in color, playing role.
fun Point2.named(label: String) A lettered point.
fun meet(a: Shape2, b: Shape2, what: String = "construction"): List<Point2> Where a and b cross; a branch where they do not is dropped (or, called plainly, fails naming what).
fun Point2.dist(o: Point2) The distance from this point to o.
fun eq(x: Double, y: Double) Equal to a relative tolerance, for check.
fun angleAt(v: Point2, a: Point2, b: Point2) The angle at v between the arms through a and b, in [0, π].
fun centroid(vararg ps: Point2) The mean of ps.
fun isSquare(q: Quadrilateral2) True when q's four sides are equal and its diagonal is one side times √2, for check.
fun isRegular(p: Polygon2, n: Int) True when p has n equal, non-zero sides and every interior angle of an n-gon, for check.
fun far(points: List<Point2>, from: Point2) Of points, the one farthest from from: where a line through from leaves a circle.
fun offset(o: Point2, p: Point2, q: Point2) How far round the circle about o, counter-clockwise from p, the point q is, in [0, 2π).
fun aroundCentre(o: Point2, at: Double, points: List<Point2>) The polygon through points in order round o, starting at angle at.
fun step(text: String, vararg figures: Figure) One numbered sentence of the proof, drawing figures.
fun <T> pick(options: List<T>, preferred: T? = null): T One of options. A plain call takes preferred (or the first); inside Elements.solutions every option is tried in turn, preferred first.
fun pickSide(line: Line2, points: List<Point2>, side: Int) Of points, the one on the side (+1 left, -1 right) of line by default; the others when searching.
fun check(ok: Boolean, what: String) Drops this branch unless ok: the proposition's claim, checked on the geometry it made.
fun <T> by(cited: () -> Construction<T>): T Cites an earlier proposition: its steps, unlettered, under its name. Returns its result. Inside Elements.solutions, each distinct valid outcome of the cited proposition is one choice here.

Elements

object Elements

Declaration Summary
fun <T> solutions(limit: Int = 64, proposition: Elements.() -> Construction<T>): List<Construction<T>> Every distinct way proposition can come out, the usual one first.
fun prop1(ab: Segment2, side: Int = 1): Construction<Triangle2> I.1. On a given finite straight line to construct an equilateral triangle. The apex is on the left of ab going from a to b when side is +1, on the right when -1.
fun prop2(a: Point2, bc: Segment2): Construction<Segment2> I.2. To place at a given point a a straight line equal to a given straight line bc.
fun prop3(greater: Segment2, less: Segment2): Construction<Segment2> I.3. From the greater of two given straight lines to cut off a part equal to the less.
fun prop9(vertex: Point2, b: Point2, c: Point2): Construction<Ray2> I.9. To bisect a given rectilineal angle: the angle at vertex between the arms through b and c.
fun prop10(ab: Segment2): Construction<Point2> I.10. To bisect a given finite straight line.
fun prop11(ab: Segment2, c: Point2, side: Int = 1): Construction<Ray2> I.11. To draw a straight line at right angles to a given straight line ab from a given point c on it. The perpendicular goes to the left of a→b when side is +1.
fun prop12(ab: Line2, c: Point2): Construction<Segment2> I.12. To draw a perpendicular to a given infinite straight line ab from a given point c not on it.
fun prop23(along: Ray2, vertex: Point2, d: Point2, e: Point2, side: Int = 1): Construction<Ray2> I.23. On a given straight line along, at the point on it, to construct an angle equal to the angle at vertex between the arms through d and e. The new arm is on the left of along when side is +1.
fun prop31(a: Point2, bc: Segment2): Construction<Line2> I.31. Through a given point a to draw a straight line parallel to a given straight line bc.
fun prop46(ab: Segment2, side: Int = 1): Construction<Quadrilateral2> I.46. On a given straight line to describe a square, on the left of a→b when side is +1.
fun book3prop1(circle: Circle2): Construction<Point2> III.1. To find the centre of a given circle (drawn, so its centre is not consulted).
fun book4prop4(t: Triangle2): Construction<Circle2> IV.4. In a given triangle to inscribe a circle.
fun book4prop5(t: Triangle2): Construction<Circle2> IV.5. About a given triangle to circumscribe a circle.
fun book6prop13(ac: Segment2, b: Point2): Construction<Segment2> VI.13. To two given straight lines to find a mean proportional: ac is the two lines laid end to end, meeting at b. The result BD has BD² = AB · BC.
fun prop47(t: Triangle2): Construction<Triple<Quadrilateral2, Quadrilateral2, Quadrilateral2>> I.47. In right-angled triangles the square on the side opposite the right angle equals the squares on the sides containing it. The right angle of t is at t.a. The result is the three squares: on BC, on BA and on AC.
fun book2prop5(ab: Segment2, side: Int = 1): Construction<Polygon2> II.5. If a straight line ab be cut into equal and unequal segments, the rectangle on the unequal segments with the square on the line between the points of section equals the square on the half. The result is the gnomon, the L-shaped region equal to the rectangle.
fun book2prop6(ab: Segment2, side: Int = 1): Construction<Polygon2> II.6. If a straight line ab be bisected and produced, the rectangle on the whole line with the added part, and the added part, with the square on the half, equals the square on the half with the added part. The result is the gnomon.
fun book2prop11(ab: Segment2, side: Int = 1): Construction<Point2> II.11. To cut a given straight line ab so that the rectangle on the whole and one segment equals the square on the other: the golden section. The result is the point of section H, with AH² = AB·BH.
fun book2prop14(rect: Quadrilateral2): Construction<Quadrilateral2> II.14. To construct a square equal to a given rectilineal figure. Euclid first makes a rectangle equal to the figure (I.45); this starts from that rectangle, rect, whose corners in order are B, E, D, C. The result is the square on EH.
fun book3prop17(circle: Circle2, a: Point2): Construction<Segment2> III.17. From a given point a to draw a straight line touching a given circle.
fun book4prop10(ab: Segment2): Construction<Triangle2> IV.10. To construct an isosceles triangle having each of the angles at the base double the remaining one: the golden triangle, apex ab.a, with sides AB = AD.
fun book4prop11(circle: Circle2, at: Double = -PI / 2): Construction<Polygon2> IV.11. In a given circle to inscribe an equilateral and equiangular pentagon, with a corner at angle at from the centre.
fun book4prop15(circle: Circle2, at: Double = -PI / 2): Construction<Polygon2> IV.15. In a given circle to inscribe an equilateral and equiangular hexagon, a corner at angle at.
fun book4prop16(circle: Circle2, at: Double = -PI / 2): Construction<Polygon2> IV.16. In a given circle to inscribe a fifteen-angled figure, equilateral and equiangular.
fun book6prop9(ab: Segment2, parts: Int = 3): Construction<Segment2> VI.9. From a given straight line ab to cut off a prescribed part, a parts-th.
fun book6prop10(ab: Segment2, ac: Segment2, cuts: List<Point2>): Construction<List<Point2>> VI.10. To cut a given uncut straight line ab similarly to a given cut straight line ac (which starts at A too), cut at cuts. The result is the points of section on AB, in order.
fun book6prop11(ab: Segment2, ac: Segment2): Construction<Segment2> VI.11. To two given straight lines ab and ac, set at an angle from A, to find a third proportional: CE, with AB : AC = AC : CE.
fun book6prop12(first: Segment2, second: Segment2, third: Segment2, at: Point2 = Point2.ORIGIN): Construction<Segment2> VI.12. To three given straight lines first, second and third to find a fourth proportional HL, with first : second = third : HL. The working is laid out from at.
fun book6prop30(ab: Segment2, side: Int = 1): Construction<Point2> VI.30. To cut a given finite straight line ab in extreme and mean ratio. Euclid applies to AB a rectangle equal to its square and exceeding by a square (VI.29); for a square that comes down to the circle drawn here. The result is the point of section E, with AB : AE = AE : EB.

Euclid.kt

Plane geometry in the classical sense: points, lines, rays, segments, circles, arcs and polygons, with every pairwise intersection, closest point and distance, and the standard constructions (perpendicular bisectors, angle bisectors, circumcircles, incircles, tangents).

val c = Circle2(point2(0, 0), 50.0)
val s = Segment2(point2(-80, 10), point2(80, 30))
val hits = s.intersect(c).points           // the two places the segment crosses the circle
val (onS, onC) = s.closestPoints(Circle2(point2(0, 200), 20.0))
lines(c.toFLine(), s.toFLine())

Everything is immutable and computed in Double. Vec2 converts in and out with Vec2.toPoint2() / Point2.toVec2(). Every shape implements Shape2, so intersect, closestPoints and distanceTo work for any pair, including a shape with itself.

Robustness: comparisons use a distance tolerance of Euclid.epsilon times the size of the inputs (one plus the largest coordinate involved, and any radius), so tangencies, touching endpoints and collinear or co-circular overlaps are reported as such rather than lost to rounding. Overlaps come back as shapes in Intersection2.overlaps (a shared piece of a line or of a circle), not as a single point. Degenerate inputs are treated as what they are: a zero-length segment or zero-radius circle behaves as a point.

Arcs run from Arc2.start through a signed Arc2.sweep in radians: positive is counter-clockwise in a y-up frame (clockwise on a y-down screen).

src/jvmMain/kotlin/afield/geometry/euclid/Euclid.kt — read the source

Used in: Euclid's Elements

Top-level functions

Declaration Summary
fun point2(x: Number, y: Number) A Point2 from any numbers.
fun Vec2.toPoint2() This vector as a Point2, in Double.

Euclid

object Euclid

Tolerance settings shared by every Euclid computation.

Declaration Summary
var epsilon Relative distance tolerance; scaled by the magnitude of the inputs of each computation.

Point2

data class Point2(val x: Double, val y: Double) : Shape2

A point (or vector) in the plane.

Declaration Summary
operator fun plus(o: Point2)
operator fun minus(o: Point2)
operator fun times(s: Double)
operator fun div(s: Double)
operator fun unaryMinus()
infix fun dot(o: Point2)
infix fun cross(o: Point2) The z component of the 3D cross product: positive when o is counter-clockwise of this.
fun normalized(): Point2 Unit vector in this direction; the zero vector stays zero.
fun rotated(radians: Double, about: Point2 = ORIGIN): Point2 This point turned radians counter-clockwise (in a y-up frame) about about.
fun lerp(o: Point2, t: Double) The point t of the way from this to o.
fun midpoint(o: Point2)
fun toVec2() This point as a Vec2, in Float.
override fun toFLine(extent: Double): FLine

Shape2

sealed interface Shape2

Anything Euclid can intersect and measure against anything else: Point2, Line2, Ray2, Segment2, Circle2, Arc2 and Polygon2 (which is measured by its boundary).

Declaration Summary
fun intersect(other: Shape2): Intersection2 Where this meets other: crossing and touching points, plus any shared stretch.
fun intersects(other: Shape2): Boolean True when this and other share at least one point.
fun closestPoint(p: Point2): Point2 The point of this shape nearest p.
fun closestPoints(other: Shape2): Pair<Point2, Point2> A nearest pair of points, the first on this shape and the second on other.
fun distanceTo(other: Shape2): Double The shortest distance between this and other; zero when they meet.
fun toFLine(extent: Double = 1000.0): FLine An FLine drawing of this shape; unbounded lines and rays are drawn extent long each way.

Line2

data class Line2(val point: Point2, val direction: Point2) : Shape2

The infinite line through point in direction (any nonzero length).

Declaration Summary
fun at(t: Double) point plus t times direction, so t counts whole direction vectors rather than distance.
fun signedDistance(p: Point2) Distance to p, positive on the left of direction (counter-clockwise side).
fun side(p: Point2): Int +1 left of the line, -1 right, 0 on it (within tolerance).
fun project(p: Point2) The foot of the perpendicular from p; same as closestPoint.
fun reflect(p: Point2) p mirrored across this line.
fun perpendicularThrough(p: Point2)
fun parallelThrough(p: Point2)
fun offset(distance: Double) The line moved distance to its left (negative moves it right).
override fun toFLine(extent: Double): FLine

Ray2

data class Ray2(val origin: Point2, val direction: Point2) : Shape2

The half-line from origin in direction.

Declaration Summary
fun at(t: Double) origin plus t times direction; the ray itself is t of zero and above.
override fun toFLine(extent: Double): FLine

Segment2

data class Segment2(val a: Point2, val b: Point2) : Shape2

The finite segment from a to b.

Declaration Summary
fun at(t: Double) The point t of the way from a to b; the segment itself is t in 0..1.
fun reversed()
override fun toFLine(extent: Double): FLine

Circle2

data class Circle2(val center: Point2, val radius: Double) : Shape2

The circle of radius about center.

Declaration Summary
fun at(angle: Double)
operator fun contains(p: Point2) True when p is inside or on the circle.
fun tangentPointsFrom(p: Point2): List<Point2> The points where the two tangent lines from p touch this circle (one if p is on it).
fun tangentLinesFrom(p: Point2): List<Line2> The tangent lines from p; the single tangent at p when it lies on the circle.
fun tangentAt(angle: Double) The tangent line at angle.
fun commonTangents(other: Circle2): List<Segment2> Every line tangent to both circles (up to four: two outer, two inner), each as the segment from its touching point on this circle to its touching point on other. Where the circles touch, the shared tangent there comes back as a zero-length segment.
override fun toFLine(extent: Double): FLine

Arc2

data class Arc2(val center: Point2, val radius: Double, val start: Double, val sweep: Double) : Shape2

The part of the circle about center of radius from angle start through signed sweep (radians, positive counter-clockwise). A sweep of 2π or more is the whole circle.

Declaration Summary
fun at(t: Double)
fun reversed()
fun containsAngle(angle: Double) True when the direction angle (from center) falls within the arc.
override fun toFLine(extent: Double): FLine

Polygon2

data class Polygon2(val points: List<Point2>) : Shape2

The closed polygon through points in order. Intersections and distances are with its boundary, its edges; Polygon2.contains asks about its inside.

Declaration Summary
operator fun contains(p: Point2): Boolean True when p is inside the polygon or on its boundary (even-odd rule).
override fun toFLine(extent: Double): FLine

Intersection2

data class Intersection2(val points: List<Point2> = emptyList(), val overlaps: List<Shape2> = emptyList())

The result of intersecting two shapes: isolated crossing or touching points, and any stretch the shapes share in overlaps (a Segment2, Ray2, Line2, Arc2 or Circle2).

GeometryBuilder.kt

A recipe language for triangle meshes: emit triangles through a transform stack with a current colour and a current material, and get back a scene of named, shaded parts.

GeometryBuilder is the language and GeometryScene is what a recipe builds: GeometryParts, each an unindexed TriMesh with its own GeometryMaterial and a normal per triangle, plus the GeometryView that says how the scene asks to be shown. The usual way to run a recipe is afield.graphics.three.geometry, which draws it in a studio-lit three view in a box output, or afield.graphics.three.animateGeometry, which runs it again every frame with the recipe's t moving. geometryScene builds the same scene with no viewer, for code that wants the mesh rather than the picture.

From there, GeometryScene.toTriMesh joins the parts into one mesh for the rest of afield.geometry.mesh, writeGlb writes a still as glTF, and writeGlbAnimation records a recipe played over time, which needs every frame to emit the same parts with the same vertex counts.

src/jvmMain/kotlin/afield/geometry/mesh/GeometryBuilder.kt — read the source

Top-level functions

Declaration Summary
fun geometryScene(t: Double = 0.0, frame: Int = 0, dt: Double = 1.0 / 60, block: GeometryBuilder.() -> Unit): GeometryScene Build a GeometryScene from a recipe, with no viewer. afield.graphics.three.geometry is the same recipe drawn in a box output.

GeometryBuilder

class GeometryBuilder(val t: Double = 0.0, val frame: Int = 0, val dt: Double = 1.0 / 60)

A stateful, immediate-mode triangle language: describe a model as a recipe of triangles, emitted through a transform stack with a current colour and a current material, and get back a GeometryScene of named, coloured, materialled TriMesh parts.

Declaration Summary
val t: Double Seconds since an animation started; 0 for a still. See afield.graphics.three.animateGeometry.
val frame: Int Frames since an animation started.
val dt: Double Seconds between frames.
val TAU: Double A full turn, 2π.
var triangleCount: Int Triangles emitted so far.
fun view(background: String? = null, ground: Boolean? = null, grid: Boolean? = null, groundY: Double? = null) How the scene is shown: the background, and whether the studio ground and its grid are drawn. Arguments left out keep their current value.
fun mesh(name: String) Name the part that follows. Triangles already emitted keep the name they had.
fun mat(color: String? = null, roughness: Double? = null, metalness: Double? = null, emissive: String? = null, emissiveIntensity: Double? = null, opacity: Double? = null, side: GeometrySide? = null, wireframe: Boolean? = null, flat: Boolean? = null) Change the material for triangles that follow. Arguments left out keep their current value.
fun mat(color: Vec4, roughness: Double? = null, metalness: Double? = null, emissive: Vec4? = null, emissiveIntensity: Double? = null, opacity: Double? = null, side: GeometrySide? = null, wireframe: Boolean? = null, flat: Boolean? = null) mat with the base colour as a Vec4 (an afield.graphics.color.hsl, say).
fun color(c: Vec4) The colour for vertices that follow, unless a vertex carries its own.
fun color(css: String) color from a CSS hex string, "#rgb" or "#rrggbb".
fun push() Save the name, material, colour and transform; pop restores them.
fun pop() Restore what the matching push saved.
fun identity() Reset the transform.
fun translate(x: Double, y: Double, z: Double) Move geometry that follows by (x, y, z), in the current frame.
fun translate(p: Vec3) Move geometry that follows by p.
fun rotateX(angle: Double) Turn geometry that follows about x, by angle radians.
fun rotateY(angle: Double) Turn geometry that follows about y, by angle radians.
fun rotateZ(angle: Double) Turn geometry that follows about z, by angle radians.
fun scale(x: Double, y: Double = x, z: Double = x) Scale geometry that follows, per axis.
fun vertex(x: Double, y: Double, z: Double, color: Vec4? = null) A point with an optional colour of its own, for tri, quad and surface. Emits nothing.
fun vertex(x: Double, y: Double, z: Double, color: String) vertex with a CSS hex colour.
fun vertex(p: Vec3, color: Vec4? = null) vertex from a Vec3.
fun mix(a: Vec3, b: Vec3, t: Double): Vec3 The point a fraction t of the way from a to b.
fun polar(radius: Double, angle: Double, y: Double = 0.0) A point radius from the y axis at angle radians round it, at height y.
fun spherical(radius: Double, theta: Double, phi: Double) A point on a sphere: theta round the y axis, phi down from the pole.
fun profile(r: Double, y: Double, color: Vec4? = null) A point on a revolve profile: r from the axis at height y.
fun profile(r: Double, y: Double, color: String) profile with a CSS hex colour.
fun section(pos: Vec3, radius: Double, color: Vec4? = null) One ring of a tube: its centre, its radius and an optional colour.
fun tri(a: GeometryVertex, b: GeometryVertex, c: GeometryVertex, flip: Boolean = false) One triangle. Counter-clockwise seen from the front; flip reverses it.
fun tri(a: Vec3, b: Vec3, c: Vec3, flip: Boolean = false) One triangle in the current colour.
fun quad(a: GeometryVertex, b: GeometryVertex, c: GeometryVertex, d: GeometryVertex) Two triangles, (a, b, c) and (a, c, d): corners in order round the quad.
fun quad(a: Vec3, b: Vec3, c: Vec3, d: Vec3) quad in the current colour.
fun surface(cols: Int, rows: Int, sampler: (u: Double, v: Double) -> GeometryVertex) A grid of cols × rows points, sampled at u and v from 0 to 1, joined into quads. The sampler returns a vertex, usually with a colour.
fun revolve(profile: List<GeometryProfilePoint>, segments: Int = 32) A lathe: the profile points (radius and height) turned round the y axis in segments steps.
fun tube(segments: Int, sides: Int, closed: Boolean = true, sampler: (t: Double) -> GeometryTubeSection) A tube: rings of sides corners swept along a curve sampled at segments values of t in 0..1. The sampler returns a section. closed joins the last ring back to the first, for a loop such as a knot; an open tube is a branch or a stem.
fun box(center: Vec3, size: Vec3) An axis-aligned box of size centred on center, in the current colour and transform.
fun box(center: Vec3, size: Double) A cube of side size centred on center.
fun seed(value: Int) Restart rand from value, so the same seed gives the same model.
fun rand(): Double A random number in 0..1.
fun rand(max: Double): Double A random number in 0..max.
fun rand(min: Double, max: Double): Double A random number in min..max.
fun randInt(min: Int, max: Int): Int A random whole number from min to max, both included.
fun noise(x: Double, y: Double): Double Smooth 2D Perlin noise, roughly in -1..1; the same everywhere for the same x, y.
fun build(): GeometryScene The scene so far.

GeometrySide

enum class GeometrySide

Which faces of a GeometryBuilder part draw.

Constants: FRONT, BACK, DOUBLE.

GeometryMaterial

data class GeometryMaterial(val color: FloatArray = floatArrayOf(1f, 1f, 1f), val roughness: Double = 0.68, val metalness: Double = 0.04, val emissive: FloatArray = floatArrayOf(0f, 0f, 0f), val emissiveIntensity: Double = 0.0, val opacity: Double = 1.0, val side: GeometrySide = GeometrySide.DOUBLE, val wireframe: Boolean = false, val flat: Boolean = true)

A part's material: what GeometryBuilder.mat set. color and emissive are sRGB, as written; linearColor and linearEmissive are what a renderer shades with.

Declaration Summary
val color: FloatArray The base colour as sRGB r, g, b in 0..1; the part's vertex colours multiply it.
val roughness: Double 0 is a mirror, 1 is chalk.
val metalness: Double 0 is a dielectric (plastic, clay, wood), 1 is a metal.
val emissive: FloatArray A colour the surface glows with, sRGB, scaled by emissiveIntensity.
val emissiveIntensity: Double How strongly emissive glows. 0 leaves the surface unlit from within.
val opacity: Double Below 1 is see-through.
val side: GeometrySide Which faces draw; double-sided by default, as in the toolbox.
val wireframe: Boolean Draw the triangle edges over the surface.
val flat: Boolean Shade each triangle flat rather than smoothing across them.
val linearColor: FloatArray The base colour, linear.
val linearEmissive: FloatArray The emissive colour, linear.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

GeometryVertex

class GeometryVertex(val x: Double, val y: Double, val z: Double, val color: FloatArray?)

A point with an optional colour (sRGB), as GeometryBuilder.vertex makes it.

GeometryProfilePoint

class GeometryProfilePoint(val r: Double, val y: Double, val color: FloatArray?)

A point on a GeometryBuilder.revolve profile.

GeometryTubeSection

class GeometryTubeSection(val x: Double, val y: Double, val z: Double, val radius: Double, val color: FloatArray?)

One ring of a GeometryBuilder.tube.

GeometryView

data class GeometryView(val background: String = "#050607", val ground: Boolean = true, val grid: Boolean = true, val groundY: Double = -1.25)

How a GeometryScene asks to be shown. See GeometryBuilder.view.

Declaration Summary
val background: String The background, as a CSS hex string.
val ground: Boolean Whether the studio ground is drawn.
val grid: Boolean Whether the grid on the ground is drawn.
val groundY: Double The ground's height. The toolbox's presets are all built to stand on -1.25.

GeometryPart

class GeometryPart(val name: String, val material: GeometryMaterial, val mesh: TriMesh)

One named part of a GeometryScene: a triangle soup with face normals and linear vertex colours.

Declaration Summary
override fun toString()

GeometryScene

class GeometryScene(val parts: List<GeometryPart>, val view: GeometryView)

What a GeometryBuilder recipe built: its parts and how it asks to be shown.

Declaration Summary
val parts: List<GeometryPart> The parts, in the order they were emitted.
val view: GeometryView How the scene asks to be shown.
val triangleCount: Int Triangles across every part.
val vertexCount: Int Vertices across every part: three per triangle, since the parts are unindexed.
fun toTriMesh(): TriMesh Every part in one mesh, materials aside.
fun sameTopology(other: GeometryScene): Boolean True when other has the same parts with the same vertex counts — what an animation export needs.
override fun toString()

GeometryGlb.kt

The exit from GeometryBuilder to a file every other tool can open.

A GeometryScene becomes one .glb with a node per part; a recipe whose vertices move becomes an animated one. toGlb returns the bytes and writeGlb puts them in a file; geometryAnimationGlb and writeGlbAnimation do the same for a run of frames. Both writers compress by default, through Gltfpack when it is installed.

The animation is stored as morph targets rather than a skeleton: one target per frame after the first, played by a weights animation. That needs no rig, which is what makes it open anywhere, and it is why every frame must emit the same parts with the same vertex counts. A morph target is a vertex-for-vertex delta against frame 0, so there is nowhere to put a triangle that appeared halfway through.

A part carries its positions, face normals, linear vertex colours and a metallic-roughness material built from its GeometryMaterial. KHR_materials_emissive_strength is declared only when some channel of a part's emissive colour, times its intensity, exceeds 1.

src/jvmMain/kotlin/afield/geometry/mesh/GeometryGlb.kt — read the source

Top-level functions

Declaration Summary
fun GeometryScene.toGlb(): ByteArray A GeometryScene as a binary glTF (.glb): one node per part, each with its positions, face normals, linear vertex colours and a metallic-roughness material made from its GeometryMaterial.
fun GeometryScene.writeGlb(file: File, compress: Boolean = true): File Write toGlb to file, returning it. With compress, the GLB goes through Gltfpack when it is installed and makes the file smaller, as the toolbox's export does; otherwise it is written as it is.
fun geometryAnimationGlb(frames: Int = 90, fps: Double = 30.0, block: GeometryBuilder.() -> Unit): ByteArray Record a GeometryBuilder recipe as an animated glTF: frames frames at fps, from t = 0, stored as morph targets (one per frame after the first, positions and normals) played by a weights animation — what three.js, Blender and every glTF viewer play back.
fun writeGlbAnimation(file: File, frames: Int = 90, fps: Double = 30.0, compress: Boolean = true, block: GeometryBuilder.() -> Unit): File Write geometryAnimationGlb to file, returning it; compress as in writeGlb.

Gltfpack.kt

The optional gltfpack step between a GLB and the disk.

Field writes binary glTF in two places, and both offer the bytes here first: writeGlb, for a geometry recipe, and the three viewer's Export GLB, which posts to POST /three-gltfpack. The object below is all there is to it: where to find a gltfpack, whether there is one, and two ways to run it.

Nothing breaks without the tool. compressIfSmaller, which the file writers use, returns the GLB it was handed when gltfpack is missing, fails, or saves nothing, and the route answers 404 so the viewer keeps its uncompressed export. compress is the strict version, for a caller that would rather hear about the failure.

src/jvmMain/kotlin/afield/geometry/mesh/Gltfpack.kt — read the source

Gltfpack

object Gltfpack

gltfpack, meshoptimizer's glTF optimiser, for making an exported GLB small: it quantises and meshopt-compresses the buffers (-cc), keeping node and material names (-kn, -km) — the settings the madp geometry toolbox exports with.

Declaration Summary
val DEFAULT_ARGS: List<String> The arguments the toolbox's export uses.
fun executable(): File? The gltfpack Field will run, or null when there is none.
val available: Boolean Whether a gltfpack is installed.
fun compress(glb: ByteArray, args: List<String> = DEFAULT_ARGS, timeoutSeconds: Long = 60): ByteArray glb through gltfpack with args. Throws if there is no gltfpack (see available) or it fails, with gltfpack's own message.
fun compressIfSmaller(glb: ByteArray, args: List<String> = DEFAULT_ARGS): ByteArray compress when gltfpack is installed and makes glb smaller; otherwise glb as it was.

MeasurementHelpers.kt

Small helpers for checking a mesh measurement against a known answer in a box.

compare prints a measured value next to its analytic value with absolute and relative error, integrate sums a per-vertex field over a TriMesh using its mixed vertex areas, and randomPoints draws repeatable uniform samples from an axis-aligned box.

src/jvmMain/kotlin/afield/geometry/mesh/MeasurementHelpers.kt — read the source

Top-level functions

Declaration Summary
fun Box.compare(label: String, measured: Number, analytic: Number) Print a measured value beside its analytic answer, absolute error and (for a nonzero answer) relative error. Numeric text is locale-independent; a zero answer reports absolute error only.
fun TriMesh.integrate(field: DoubleArray): Double Integrate one scalar per vertex using mixedVertexAreas, the same weights as the curvature operators.
fun TriMesh.integrate(field: FloatArray): Double integrate for a float-valued vertex field; accumulation remains in double precision.
fun randomPoints(count: Int, seed: Long = 0L, bounds: AxisAlignedBoundingBox3D): List<Vec3> Uniform points in an explicit axis-aligned 3D box. Uses a fresh seeded Java Random and three nextFloat samples per point, so calls are repeatable without changing any shared random state. Bounds may have zero extent along an axis; their coordinates must be finite and ordered.

mesh.kt

TriMesh — the CPU triangle mesh Field's 3D geometry family is built on, plus the builder that makes one and the whole-mesh transforms that move one around.

A TriMesh is not a renderer buffer (that is Field 4's afield.graphics.render.Mesh) and not a graph of vertex objects. It is flat FloatArray attribute storage plus an IntArray of triangle indices: the layout every algorithm in this family wants, the layout a GPU upload wants, and cheap to copy. Positions are required; normals, uvs and per-vertex colours are optional and either absent or complete.

The reason the vertices are floats-in-an-array rather than List<Vec3> is that Vec3 is a mutable Vector3f. A mesh holding shared mutable vectors is a mesh where mesh.position(0).y += 1 silently edits the mesh — or worse, edits three triangles because the vector was shared. So every accessor here (position, normal, uv, color) returns a fresh vector you own, and the …Into variants let a hot loop reuse one.

The family

file what it adds
mesh.kt this: the type, TriMeshBuilder / triMesh, transforms, concatenation
mesh_topology.kt MeshTopology — adjacency, boundary loops, components, manifoldness, Euler characteristic / genus, TriMesh.weld, TriMesh.fixWinding
mesh_generate.kt boxMesh, uvSphereMesh, icosphereMesh, torusMesh, cylinderMesh, extrudeMesh, revolveMesh, tubeMesh, heightfieldMesh, reliefMesh
mesh_sdf.kt ScalarField3D, the SDF primitives / combinators, and marchingCubes
mesh_process.kt TriMesh.loopSubdivide, TriMesh.laplacianSmooth, TriMesh.taubinSmooth, TriMesh.withVertexNormals, TriMesh.decimate
mesh_measure.kt TriMesh.surfaceArea, TriMesh.signedVolume, curvature, MeshBvh (rays, closest point, inside test)
mesh_io.kt OBJ and STL, read and write
mesh_field.kt the bridges: TriMesh.toFLines, VoronoiCell3.toTriMesh, Box.showMesh

Conventions

  • Winding is counter-clockwise seen from outside, so faceNormal points out of a closed solid and signedVolume of one is positive. Everything in the family agrees on this, and TriMesh.fixWinding repairs a mesh that does not.
  • Nothing here mutates: every operation returns a new TriMesh. The arrays are vals and are not copied on construction, so do not hand a builder's scratch array to the constructor and then keep writing to it — use TriMeshBuilder.build.
  • No dependencies beyond the Kotlin stdlib and Field's own generated linalg. No GPU, no AWT, no model-format libraries.
val ball = icosphereMesh(subdivisions = 3, radius = 1.0)
val shell = ball.scaled(1.0, 0.6, 1.0).withVertexNormals()
println("${shell.triangleCount} tris, volume ${shell.signedVolume()}")
display(shell.toFLines())            // draw its wireframe

src/jvmMain/kotlin/afield/geometry/mesh/mesh.kt — read the source

Used in: Meshes and 3D geometry

Top-level functions

Declaration Summary
inline fun triMesh(block: TriMeshBuilder.() -> Unit): TriMesh Run block against a fresh TriMeshBuilder and TriMeshBuilder.build the result.

TriMesh

class TriMesh(val positions: FloatArray, val indices: IntArray, val normals: FloatArray? = null, val uvs: FloatArray? = null, val colors: FloatArray? = null) : ToHTML

An indexed triangle mesh: vertexCount * 3 position floats, triangleCount * 3 indices, and optional matching normal / uv / colour arrays.

Declaration Summary
val positions: FloatArray
val indices: IntArray
val normals: FloatArray?
val uvs: FloatArray?
val colors: FloatArray?
val vertexCount: Int Number of vertices.
val triangleCount: Int Number of triangles.
val isEmpty: Boolean True when there are no triangles — an empty mesh is legal and is what a degenerate march returns.
val hasNormals: Boolean True when this mesh carries per-vertex normals.
val hasUvs: Boolean True when this mesh carries per-vertex texture coordinates.
val hasColors: Boolean True when this mesh carries per-vertex colours.
fun x(i: Int): Float Vertex i's x, without allocating.
fun y(i: Int): Float Vertex i's y, without allocating.
fun z(i: Int): Float Vertex i's z, without allocating.
fun position(i: Int): Vec3 A fresh Vec3 holding vertex i's position — mutating it does not touch the mesh.
fun positionInto(i: Int, dest: Vec3): Vec3 position into a vector you already own, for loops that must not allocate. Returns dest.
fun normal(i: Int): Vec3? Vertex i's normal, or null when the mesh has none. Fresh vector.
fun uv(i: Int): Vec2? Vertex i's texture coordinate, or null when the mesh has none. Fresh vector.
fun color(i: Int): Vec3? Vertex i's colour, or null when the mesh has none. Fresh vector.
fun ia(t: Int): Int Triangle t's first vertex index.
fun ib(t: Int): Int Triangle t's second vertex index.
fun ic(t: Int): Int Triangle t's third vertex index.
fun triangle(t: Int): Triple<Vec3, Vec3, Vec3> Triangle t's three corner positions, as fresh vectors.
fun faceNormal(t: Int): Vec3 Triangle t's geometric normal from its winding — normalize((b-a) x (c-a)).
fun triangleArea(t: Int): Double Triangle t's area — half the length of the cross product, in double precision.
fun vertices(): List<Vec3> All vertex positions as fresh vectors — convenient, but allocates one per vertex.
fun triangleIndices(): List<Triple<Int, Int, Int>> Every triangle's (a, b, c) index triple.
fun isFinite(): Boolean True when every stored float is finite — the cheapest "did my generator go wrong" check.
fun copy(positions: FloatArray = this.positions, indices: IntArray = this.indices, normals: FloatArray? = this.normals, uvs: FloatArray? = this.uvs, colors: FloatArray? = this.colors): TriMesh A copy with some arrays replaced; anything not passed is kept. Passing null for an attribute drops it, so copy(normals = null) is this mesh without normals.
fun withoutAttributes(): TriMesh Positions and indices only — drops normals, uvs and colours.
fun withNormals(normals: FloatArray): TriMesh This mesh with normals attached (one xyz per vertex). See withVertexNormals to compute them.
fun withUvs(uvs: FloatArray): TriMesh This mesh with uvs attached (one uv per vertex).
fun withColors(colors: FloatArray): TriMesh This mesh with colors attached (one linear rgb per vertex).
fun withColor(r: Number, g: Number, b: Number): TriMesh This mesh painted a single flat colour — the fast way to make a preview readable.
fun coloredBy(f: (Vec3) -> Vec3): TriMesh This mesh with per-vertex colours from a function of position.
fun transformed(f: (Vec3) -> Vec3): TriMesh Every vertex position through f, indices unchanged.
fun translated(dx: Number, dy: Number, dz: Number): TriMesh Translated by (dx, dy, dz). Normals survive — translation does not affect them.
fun translated(d: Vec3): TriMesh Translated by a vector.
fun scaled(sx: Number, sy: Number, sz: Number): TriMesh Scaled about the origin, per axis.
fun scaled(s: Number): TriMesh Uniformly scaled about the origin.
fun rotated(q: Quat): TriMesh Rotated by a Quat. Rigid, so the normals are rotated with the positions rather than dropped.
fun flipped(): TriMesh Every triangle wound the other way, and every normal negated — an inside-out copy.
operator fun plus(other: TriMesh): TriMesh Two meshes as one, other's indices shifted past this one's vertices.
fun filterTriangles(keep: (Int) -> Boolean): TriMesh A mesh made of the triangles keep accepts, with the now-unused vertices removed and the indices renumbered. Attributes follow their vertices.
override fun toString(): String
override fun toHTML(): String Inline thumbnail for the value visualizer: the mesh actually rasterised, shaded and depth-buffered, plus a one-line measurement caption. Evaluating a TriMesh in a box (or mesh.h) shows the shape rather than TriMesh@3f1a2b.

TriMeshBuilder

class TriMeshBuilder

The accumulator every generator in this family is written against: append vertices, append triangles that reference them, build.

Declaration Summary
val vertexCount: Int How many vertices have been appended so far — also the index the next vertex will get.
val triangleCount: Int How many triangles have been appended so far.
fun group(name: String) Name the piece every triangle appended from here on belongs to.
fun vertex(p: Vec3, normal: Vec3? = null, uv: Vec2? = null, color: Vec3? = null): Int Append a vertex and return its index.
fun vertex(x: Number, y: Number, z: Number, normal: Vec3? = null, uv: Vec2? = null, color: Vec3? = null): Int vertex from loose floats — what the generators use, so they never allocate a Vec3 per vertex.
fun triangle(a: Int, b: Int, c: Int) Append one triangle. Counter-clockwise seen from outside.
fun quad(a: Int, b: Int, c: Int, d: Int) Append a quad as two triangles: (a,b,c) and (a,c,d). Corners in order around the quad.
fun polygon(corners: List<Int>) Append a convex polygon as a fan from its first corner. Non-convex polygons will self-overlap — extrudeMesh triangulates properly via ear clipping for those.
fun add(mesh: TriMesh): Int Append a whole mesh, offsetting its indices. Returns the index its vertex 0 landed on.
fun build(): TriMesh Freeze into a TriMesh. The builder stays usable; the mesh gets its own arrays.

mesh_field.kt

What makes the mesh family Field's rather than a generic geometry library: the bridges out of TriMesh and into the things Field already draws, renders and displays.

Four bridges:

  1. Mesh → FLine. TriMesh.toFLines gives you the wireframe, the silhouette-free edge set, the open boundary loops or the normals as drawable lines, so a marched iso-surface goes straight into the same drawing / animation / export machinery as anything hand-drawn — and is visible in a Field document today, with no renderer set up.
  2. Mesh → pixels. TriMesh.renderPng rasterises the mesh — perspective, z-buffer, perspective-correct attributes, two-light lambert, depth-tested wireframe — by packing it for VisionGeometryPreview's renderer, which already had all of that. Box.showMesh pushes the result to a box output and TriMesh.toHTML uses it for the inline value thumbnail, so evaluating a mesh in a box shows the shape.
  3. 3D Voronoi → mesh. VoronoiCell3.toTriMesh and VoronoiLattice.toTriMesh triangulate the polygonal cells voronoi.kt already computes by half-space clipping. Field could compute a 3D Voronoi diagram and not turn it into geometry; now it can.
  4. Field lines → mesh. tubeMesh on an FLine, so a drawn stroke becomes a solid.
val cells = voronoi3d(List(24) { vec(rand(-1, 1), rand(-1, 1), rand(-1, 1)) })
val solid = cells.toTriMesh(shrink = 0.9)   // every cell, shrunk toward its own site
showMesh(solid, caption = "${solid.triangleCount} triangles from 24 sites")

src/jvmMain/kotlin/afield/geometry/mesh/mesh_field.kt — read the source

Used in: Geometry from disk, Meshes and 3D geometry, Three.js scenes with three { … }

Top-level functions

Declaration Summary
fun TriMesh.toFLines(mode: MeshLines = MeshLines.EDGES, creaseAngleDegrees: Double = 25.0, normalLength: Double = 0.03): List<FLine> This mesh as drawable FLines — the fastest way to see a mesh in a Field document without involving a renderer.
fun tubeMesh(line: FLine, radius: Number = 0.05, sides: Int = 12, samples: Int = 64, closed: Boolean = false, caps: Boolean = true): TriMesh A tube swept along an FLine, sampled at samples points — see tubeMesh.
fun VoronoiCell3.toTriMesh(shrink: Double = 1.0, fanFromCentroid: Boolean = true, healToConvexHull: Boolean = true): TriMesh One 3D Voronoi cell's polygonal faces, triangulated into a closed TriMesh.
fun VoronoiLattice.toTriMesh(shrink: Double = 0.92, boundedOnly: Boolean = false): TriMesh Every cell of a 3D Voronoi lattice as one TriMesh, one connected component per cell.
fun TriMesh.toCapturedMesh(label: String = "TriMesh", creaseAngleDegrees: Double = 35.0): VisionGeometryPreview.CapturedMesh Pack this mesh into the 16-float-per-vertex layout VisionGeometryPreview's rasterizer consumes, so the whole shaded / depth-buffered / wireframed renderer applies to a TriMesh for free.
fun TriMesh.coloredByScalar(values: DoubleArray, colormap: String = "viridis", low: Double? = null, high: Double? = null, percentile: Double = 0.02): TriMesh Per-vertex colours from a per-vertex scalar, through the same afield.graphics.color.Colormaps tables RectSL's charts and Img.colormap use — so a curvature field, a distance field and a bar chart in the same document are coloured by one source of truth.
fun TriMesh.renderPng(view: MeshView = MeshView(), caption: String? = null, title: String? = null): ByteArray This mesh rasterised to PNG bytes: perspective, z-buffer, two-light lambert shading over the per-vertex colours, and a depth-tested wireframe.
fun TriMesh.renderPixels(view: MeshView = MeshView()): VisionGeometryPreview.Raster This mesh's raster as ARGB ints plus its size — for compositing several meshes into one figure.
fun TriMesh.toFigureHtml(view: MeshView = MeshView(), caption: String? = null, title: String? = null): String A complete <figure> — this mesh rendered as an inline PNG plus its measurements as a caption — for display(…) in a box output or for the value visualizer.
fun Box.showMesh(mesh: TriMesh, channel: String = "main", view: MeshView = MeshView(), caption: String? = null, title: String? = null, renderer: MeshRenderer = MeshRenderer.GPU, height: Int? = null) Push a rendered view of mesh to one of this box's output channels, with its measurements as a caption.
fun Box.showMeshGrid(vararg panels: Pair<String, TriMesh>, channel: String = "main", columns: Int = 2, cellWidth: Int = 380, view: MeshView = MeshView(), renderer: MeshRenderer = MeshRenderer.GPU, facts: Boolean = true, sharedScale: Boolean = false) Several meshes side by side, each rendered and captioned — the fastest way to see a whole pipeline (generated / subdivided / smoothed / decimated) side by side.
fun Box.showMeshTurntable(mesh: TriMesh, channel: String = "main", views: List<MeshView> = MeshView.turntable(4), cellWidth: Int = 260, title: String? = null, renderer: MeshRenderer = MeshRenderer.GPU) A turntable contact sheet of one mesh: the same shape from views, laid out in a row.
fun TriMesh.summary(): String A one-line factual summary of a mesh: counts, extent, topology and volume — the thing to print when you want the numbers rather than the picture.
fun TriMesh.longestEdge(): Double Longest edge in the mesh — a useful scale for choosing a weld tolerance or a march resolution.
fun TriMesh.meanEdgeLength(): Double Mean edge length over the mesh's unique edges.
fun TriMesh.normalized(size: Number = 1.0): TriMesh This mesh recentred on its own bounding box and scaled so its longest axis is size.
fun TriMesh.defectFLines(diagnosis: MeshDiagnosis = diagnose(), includeWinding: Boolean = false): List<FLine> The defects TriMesh.diagnose found, as coloured FLines to draw over the mesh.
fun TriMesh.withDefectColors(diagnosis: MeshDiagnosis = diagnose()): TriMesh This mesh painted by defect, so its rendered picture shows where the trouble is.
fun Box.showMeshDiagnosis(mesh: TriMesh, channel: String = "main", view: MeshView? = null, maxDefects: Int = 8, title: String? = null) The diagnosis as a card beside the mesh: the shape with its defects painted on, and the ordered list of what is wrong and what to call about it.
fun TriMesh.defectFacingView(diagnosis: MeshDiagnosis = diagnose(), width: Int = 420, height: Int = 340): MeshView A camera pointed at this mesh's worst defect, so the picture shows the thing the caption is about.

MeshLines

enum class MeshLines

Which lines TriMesh.toFLines should produce.

Constants: EDGES, CREASES, BOUNDARY, NORMALS, FACES.

MeshView

data class MeshView(val yawDegrees: Double = 28.0, val pitchDegrees: Double = 18.0, val distance: Double = 2.7, val fieldOfViewDegrees: Double = 40.0, val width: Int = 720, val height: Int = 540, val wireframe: Boolean = true, val supersample: Int = 2, val cullBackFaces: Boolean = true, val background: Int = 0xFF101318.toInt())

How to draw a TriMesh in a preview: where the camera stands and what the frame looks like.

Declaration Summary
val yawDegrees: Double Orbit around +y, in degrees. 0 looks straight down -z.
val pitchDegrees: Double Lift above the horizon, in degrees. Positive looks down on the mesh.
val distance: Double Eye distance in multiples of the mesh's bounding-sphere radius, so it is scale-free.
val fieldOfViewDegrees: Double Vertical field of view in degrees. Smaller is a longer lens and less perspective.
val width: Int Output width in pixels.
val height: Int Output height in pixels.
val wireframe: Boolean Draw a depth-tested wireframe over the shading. On by default: it turns "a smooth blob" into "a mesh you can count the triangles in".
val supersample: Int Render at this multiple in each axis and box-filter down. 2 is a good anti-aliasing default.
val cullBackFaces: Boolean Cull back faces, as a GPU would — so a face wound the wrong way is missing here exactly as it would be there.
val background: Int Background as packed ARGB.

MeshRenderer

enum class MeshRenderer

Which renderer the showMesh family draws with.

Constants: GPU, CPU.

mesh_generate.kt

Where a TriMesh comes from: the primitives (boxMesh, uvSphereMesh, icosphereMesh, torusMesh, cylinderMesh, coneMesh, capsuleMesh, the five platonics) and the derived generators that build one out of something you already have (extrudeMesh, revolveMesh, tubeMesh, heightfieldMesh, reliefMesh, polygonMesh).

Welding and normals

Everything here is welded and watertight where it can be. A generator returns the minimal vertex set, so MeshTopology.eulerCharacteristic of a box, a sphere, a cone or a capped cylinder is exactly 2, of a torus exactly 0, and TriMesh.isWatertight is true. The volume, curvature and decimation code all need that; a mesh that merely renders is not enough for any of them.

Normals are only attached where they are unambiguous. A sphere, icosphere, torus and capsule are smooth everywhere, so they carry per-vertex normals. A box, cylinder, cone, extrusion or platonic solid has creases: the two faces meeting at an edge want different normals at the same vertex, and you cannot have both a shared vertex and two normals. Duplicating the vertex would break watertightness and averaging across the crease would round it off, so these ship with no normals and you pick:

boxMesh().flatShaded()          // one normal per triangle: hard edges, 3 verts per tri
boxMesh().shadedNormals(35.0)   // split only where faces disagree by > 35 degrees
boxMesh().withVertexNormals()   // one averaged normal per shared vertex: rounded corners

The preview / render bridges in mesh_field.kt call TriMesh.shadedNormals for you when a mesh arrives without normals, so a bare boxMesh() still looks like a box.

Orientation

+y is up, matching the preview camera. Radial generators sweep x = r cos u, z = -r sin u, which is the handedness that makes counter-clockwise-from-outside windings come out right. planeMesh and polygonMesh live in the xy plane facing +z; heightfieldMesh lives in the xz plane facing +y: a plane is a wall and a heightfield is a landscape.

src/jvmMain/kotlin/afield/geometry/mesh/mesh_generate.kt — read the source

Used in: Meshes and 3D geometry, Three.js scenes with three { … }, A photograph, closed and mounted

Top-level functions

Declaration Summary
fun boxMesh(sizeX: Number = 1.0, sizeY: Number = 1.0, sizeZ: Number = 1.0): TriMesh An axis-aligned box centred on the origin, 8 vertices and 12 triangles — the minimal watertight cube.
fun cubeMesh(size: Number = 1.0): TriMesh A cube: boxMesh with all three extents equal to size.
fun planeMesh(nx: Int = 1, ny: Int = 1, width: Number = 1.0, height: Number = 1.0): TriMesh A rectangular grid of quads in the xy plane at z = 0, facing +z, with normals and uvs.
fun uvSphereMesh(segments: Int = 32, rings: Int = 16, radius: Number = 1.0): TriMesh A latitude/longitude sphere: two pole vertices and rings - 1 rows of segments.
fun icosphereMesh(subdivisions: Int = 2, radius: Number = 1.0): TriMesh A geodesic sphere: an icosahedron with each triangle split into four subdivisions times, every vertex pushed out to radius.
fun tetrahedronMesh(radius: Number = 1.0): TriMesh A regular tetrahedron with its vertices on a sphere of radius. 4 vertices, 4 triangles, χ = 2. No normals — every face is a crease.
fun octahedronMesh(radius: Number = 1.0): TriMesh A regular octahedron with its 6 vertices on a sphere of radius. 8 triangles, χ = 2.
fun icosahedronMesh(radius: Number = 1.0): TriMesh A regular icosahedron with its 12 vertices on a sphere of radius. 20 triangles, χ = 2.
fun dodecahedronMesh(radius: Number = 1.0): TriMesh A regular dodecahedron with its 20 vertices on a sphere of radius, as 12 pentagons fanned from their own centres — 32 vertices, 60 triangles, χ = 2.
fun cylinderMesh(segments: Int = 32, radius: Number = 0.5, height: Number = 1.0, caps: Boolean = true): TriMesh A cylinder along y, centred on the origin. Watertight with caps (χ = 2); an open tube without them (χ = 0, two boundary loops).
fun coneMesh(segments: Int = 32, radius: Number = 0.5, height: Number = 1.0, cap: Boolean = true): TriMesh A cone along y with its apex at +height/2 and its base at -height/2. Watertight with cap (χ = 2). No normals — the apex is a genuine singularity.
fun torusMesh(majorSegments: Int = 48, minorSegments: Int = 24, majorRadius: Number = 1.0, minorRadius: Number = 0.35): TriMesh A torus in the xz plane: majorRadius out to the centre of the tube, minorRadius the tube itself. Watertight, smooth normals, and the family's canonical genus 1 shape — χ = 0, MeshTopology.genus 1.
fun capsuleMesh(segments: Int = 32, rings: Int = 8, radius: Number = 0.4, height: Number = 1.0): TriMesh A capsule (cylinder with hemispherical ends) along y: total length height + 2 * radius.
fun revolveMesh(profile: List<Vec2>, axis: RevolveAxis, segments: Int = 48, turns: Double = 1.0): TriMesh A lathe: a 2D profile of (radius, height) points swept around axis.
fun triangulatePolygon(polygon: List<Vec2>): List<Int> Ear-clipping triangulation of a simple (non-self-intersecting) polygon.
fun polygonMesh(polygon: List<Vec2>): TriMesh A flat, triangulated polygon in the xy plane at z = 0, facing +z. Concave polygons are handled (triangulatePolygon). A disc: χ = 1, one boundary loop.
fun extrudeMesh(polygon: List<Vec2>, height: Number = 1.0, caps: Boolean = true): TriMesh A prism: polygon in the xy plane swept along +z by height.
fun tubeMesh(path: List<Vec3>, radius: Number = 0.1, sides: Int = 12, closed: Boolean = false, caps: Boolean = true): TriMesh A round tube swept along a 3D polyline — the mesh you want for a drawn stroke, a trajectory or a graph edge in 3D.
fun heightfieldMesh(nx: Int = 64, nz: Int = 64, width: Number = 2.0, depth: Number = 2.0, height: (Float, Float) -> Float): TriMesh A landscape: a grid in the xz plane whose y comes from height, with computed normals and uvs.
fun Img.toHeightfieldMesh(scale: Number = 0.25, width: Number = 2.0, depth: Number? = null, step: Int = 1): TriMesh A heightfield sampled from an Img — one grid vertex per pixel by default, with the image's luminance (or its single channel) as y.
fun reliefMesh(nx: Int = 64, nz: Int = 64, width: Number = 2.0, depth: Number = 2.0, baseY: Number = -0.25, height: (Float, Float) -> Float): TriMesh A relief plaque: heightfieldMesh's surface, closed — skirt walls dropped to baseY and a flat base under them. Watertight, χ = 2, and therefore the first heightfield you can hand to afield.geometry.solid.manifold.Solid.
fun Img.toReliefMesh(scale: Number = 0.25, width: Number = 2.0, depth: Number? = null, step: Int = 1, baseThickness: Number = 0.1): TriMesh Img.toHeightfieldMesh's closed sibling — an image as a printable relief plaque, via reliefMesh.

RevolveAxis

enum class RevolveAxis

Which axis revolveMesh sweeps a (radius, height) profile around.

Constants: Y, Z.

mesh_io.kt

Getting a TriMesh in and out of the two formats every 3D tool on the machine can already read: OBJ (text, indexed, carries normals and uvs) and STL (ASCII or binary, triangle soup, the lingua franca of 3D printing).

Both directions, in pure Kotlin, and what each format carries is different:

  • TriMesh.toObj / objToTriMesh keep the index structure, so a watertight mesh round-trips as a watertight mesh. Normals and uvs survive; per-vertex colours do not (OBJ has no standard for them). Faces with more than three corners are fan-triangulated on read, so a quad-mesh OBJ from a modeller loads fine.
  • TriMesh.toStlAscii / TriMesh.toStlBinary / stlToTriMesh are triangle soup, because STL has no vertex sharing at all: a mesh written to STL and read back has 3 * triangleCount vertices and reports every edge as a boundary. stlToTriMesh(bytes).weld() gives you the mesh back. The read path does not weld for you, because welding needs a tolerance, and a tolerance guessed on your behalf is what loses a hairline feature.
val mesh = icosphereMesh(3)
File("/tmp/ball.obj").writeText(mesh.toObj())
File("/tmp/ball.stl").writeBytes(mesh.toStlBinary())
val back = objToTriMesh(File("/tmp/ball.obj").readText())

These two formats, and no others — see meshFromFile for where to go for PLY, glTF and GLB, and public/docs/geometry_from_disk.md for the measured account of what each format loses.

src/jvmMain/kotlin/afield/geometry/mesh/mesh_io.kt — read the source

Used in: Geometry from disk, Meshes and 3D geometry, Solids that cannot fail: Manifold in Field

Top-level functions

Declaration Summary
fun TriMesh.toObj(name: String = "mesh", decimals: Int = 6): String This mesh as Wavefront OBJ text: v positions, then vn normals and vt uvs if present, then f faces with 1-based indices.
fun objToTriMesh(text: String): TriMesh Parse Wavefront OBJ text into a TriMesh.
fun TriMesh.saveObj(file: File, name: String = file.nameWithoutExtension) Write this mesh to file as OBJ. Creates parent directories.
fun triMeshFromObj(file: File): TriMesh Read an OBJ file — see objToTriMesh for what survives.
fun TriMesh.toStlAscii(name: String = "mesh"): String This mesh as ASCII STL. One facet per triangle with its face normal, no vertex sharing (the format has none).
fun TriMesh.toStlBinary(header: String = "Field afield.stdlib mesh_io.kt"): ByteArray This mesh as binary STL: an 80-byte header, the triangle count, then 50 bytes per triangle (little-endian float normal, three float positions, a zero attribute short).
fun stlToTriMesh(bytes: ByteArray): TriMesh Parse STL, ASCII or binary — the format is sniffed, not declared.
fun TriMesh.saveStl(file: File) Write binary STL to file. Creates parent directories.
fun triMeshFromStl(file: File): TriMesh Read an STL file, ASCII or binary — see stlToTriMesh.
fun meshFromFile(path: String): TriMesh Load a mesh from a path, choosing the reader by extension (.obj, .stl).
fun requireGeometryFrom(file: File, mesh: TriMesh) Throw when a non-empty file parsed to no vertices, naming what it looks like instead. A thing.obj that is really a ZIP, a glTF or an HTML error page otherwise reads as an empty mesh and a blank panel.

mesh_measure.kt

Asking a TriMesh questions: how big is it, where is its middle, how curved is it — and, through MeshBvh, what does this ray hit, what is the nearest point on the surface, and is this point inside.

The measurements (TriMesh.surfaceArea, TriMesh.signedVolume, TriMesh.centroid) are exact for the polyhedron, computed in double precision, and cheap. TriMesh.signedVolume is the one to keep in mind: it is the divergence-theorem tetrahedron sum, so it is exact for any closed mesh, it is positive for outward winding and negative for inward, and comparing it against an analytic answer (4/3 π for a marched unit sphere) is the single most informative test a geometry pipeline can have.

The curvatures are the standard discrete operators — angle deficit for Gaussian, the cotangent-Laplacian's magnitude for mean, both divided by the mixed Voronoi area. They satisfy Gauss-Bonnet (Σ K_v A_v = 2π χ) to float precision on a closed mesh, which is how they are tested.

MeshBvh exists because the useful queries are all O(n) without it and O(log n) with it. It is a plain median-split bounding-volume hierarchy over triangles, built once, then:

val bvh = mesh.bvh()
bvh.raycast(vec(0, 0, -5), vec(0, 0, 1))?.let { println("hit at ${it.point}") }
bvh.closestPoint(vec(2, 0, 0))                 // nearest surface point + distance
bvh.contains(vec(0, 0, 0))                     // inside test, by parity of crossings
bvh.signedDistance(vec(0.5f, 0, 0))            // negative inside

src/jvmMain/kotlin/afield/geometry/mesh/mesh_measure.kt — read the source

Used in: Meshes and 3D geometry, Three.js scenes with three { … }

Top-level functions

Declaration Summary
fun TriMesh.surfaceArea(): Double Total area of every triangle, in double precision.
fun TriMesh.signedVolume(): Double Volume enclosed, as the signed sum of the tetrahedra from the origin to each triangle.
fun TriMesh.volume(): Double signedVolume's magnitude — the volume whichever way the mesh is wound.
fun TriMesh.centroid(): Vec3 The area-weighted centroid of the surface — where a hollow shell of this shape would balance.
fun TriMesh.solidCentroid(): Vec3 The centre of mass of the enclosed solid, assuming uniform density — the volume-weighted sum of the same tetrahedra signedVolume uses.
fun TriMesh.mixedVertexAreas(): DoubleArray Per-vertex mixed Voronoi area — the area each vertex "owns", the denominator both curvatures need.
fun TriMesh.gaussianCurvature(): DoubleArray Discrete Gaussian curvature per vertex: the angle deficit 2π - Σθ divided by the vertex's mixed area.
fun TriMesh.meanCurvature(): DoubleArray Discrete mean curvature per vertex, signed against the surface normal: half the magnitude of the cotangent Laplacian, divided by the mixed area.
fun TriMesh.bvh(leafSize: Int = 8): MeshBvh Build a MeshBvh over this mesh. Hold on to it — building is the expensive part.
fun TriMesh.raycast(origin: Vec3, direction: Vec3): RayHit? One-shot ray query. Builds a BVH, so use bvh directly for more than a handful of rays.
fun TriMesh.closestPoint(query: Vec3): SurfacePoint? One-shot nearest-surface-point query. Builds a BVH — see bvh.
fun TriMesh.contains(query: Vec3): Boolean One-shot inside test. Builds a BVH — see bvh.
fun TriMesh.deviationFrom(other: TriMesh): Double How far apart two meshes are, sampled: the largest distance from a vertex of other to the surface of this mesh (a one-sided, vertex-sampled Hausdorff distance).
fun maxDeviation(a: TriMesh, b: TriMesh): Double The larger of the two one-sided deviationFrom distances — symmetric, still vertex-sampled.
fun TriMesh.isContainedIn(hullCandidate: TriMesh, tolerance: Double = 1e-4): Boolean The solid angle-free, purely combinatorial check that this mesh's vertices all lie on or inside hullCandidate — used by the convex-hull tests, and occasionally useful on its own.
fun TriMesh.samplePoints(count: Int, seed: Long = 1L): List<Vec3> Uniform-ish points on this mesh's surface, chosen by area so a finely triangulated region is not over-sampled — the sampler a Monte-Carlo volume check or a point-cloud export wants.
fun convexHullMesh(points: List<Vec3>, tolerance: Double = 1e-7): TriMesh The convex hull of a point cloud, as a TriMesh — incremental construction, no dependencies.
fun TriMesh.convexHull(): TriMesh The convex hull of this mesh's own vertices — see convexHullMesh.
fun angleBetweenDegrees(a: Vec3, b: Vec3): Double The angle between two vectors in degrees, 0..180, or 0 if either has no length.

RayHit

class RayHit(val triangle: Int, val distance: Double, val point: Vec3, val normal: Vec3, val u: Double, val v: Double)

What a ray hit: the triangle, the distance along the ray, and the point and normal there.

Declaration Summary
val triangle: Int Index of the triangle that was hit.
val distance: Double Distance along the (unit-length) ray direction.
val point: Vec3 The intersection point.
val normal: Vec3 The hit triangle's geometric normal — not flipped toward the ray.
val u: Double Barycentric weight of the hit on corner b. u + v <= 1.
val v: Double Barycentric weight of the hit on corner c. u + v <= 1.
override fun toString(): String The triangle, the distance and the point, for println.

SurfacePoint

class SurfacePoint(val triangle: Int, val point: Vec3, val distance: Double)

The nearest point on a mesh to a query point.

Declaration Summary
val triangle: Int Index of the triangle the nearest point lies on.
val point: Vec3 The nearest point itself.
val distance: Double Distance from the query point to point.
override fun toString(): String The triangle, the point and the distance, for println.

MeshBvh

class MeshBvh(val mesh: TriMesh, leafSize: Int = 8)

A bounding-volume hierarchy over a mesh's triangles: build once, query many times.

Declaration Summary
fun raycast(origin: Vec3, direction: Vec3, maxDistance: Double = Double.MAX_VALUE, cullBackFaces: Boolean = false, minDistance: Double = 1e-9): RayHit? The nearest triangle direction from origin, or null.
fun raycastAll(origin: Vec3, direction: Vec3, maxDistance: Double = Double.MAX_VALUE): List<RayHit> Every triangle the ray meets, nearest first — what a "cut this solid with a line" query wants.
fun closestPoint(query: Vec3): SurfacePoint? The nearest point of the mesh's surface to query, or null for an empty mesh.
fun contains(query: Vec3): Boolean Is query inside the mesh?
fun signedDistance(query: Vec3): Double Distance to the surface, negative inside — the mesh as a ScalarField3D.
fun asSdf(): ScalarField3D This mesh as a signed-distance ScalarField3D, for composing with the sdf… combinators.
fun bounds(): AxisAlignedBoundingBox3D Bounds of the whole hierarchy — the mesh's AABB, for free.
override fun toString(): String Node and triangle counts — enough to see whether the tree is the shape you expected.

mesh_process.kt

Changing a mesh's sampling without changing the shape it stands for: normals, subdivision, smoothing and decimation.

These are the four operations that make the difference between a mesh you generated and a mesh you can use. In rough order of how often you will reach for them:

  • TriMesh.withVertexNormals / TriMesh.flatShaded / TriMesh.shadedNormals — shading. TriMesh.shadedNormals splits a vertex only where the faces meeting there genuinely disagree, so a cylinder comes out smooth around its side and hard at its rim, from one call.
  • TriMesh.loopSubdivide — four triangles for one, converging on the Loop limit surface. Refines and smooths, and preserves the Euler characteristic exactly.
  • TriMesh.laplacianSmooth / TriMesh.taubinSmooth — noise removal. Laplacian shrinks the shape (that is what a diffusion does); Taubin's λ/μ pair alternates a shrink with an expand and so does not, which is why it is the one to default to. Both offer cotangent weights, which are scale- and shape-independent where the uniform ones are not.
  • TriMesh.decimate — quadric error metric edge collapse down to a triangle budget. The dual of subdivision, and the one that has to work hardest: it maintains per-vertex quadrics, a cost-ordered queue with lazy invalidation, the link condition (so a collapse cannot make the mesh non-manifold) and a normal-flip veto (so it cannot turn the mesh inside out locally).
val fine = icosphereMesh(4)                       // 5120 triangles
val coarse = fine.decimate(400)                   // 400, and still a sphere
val smooth = coarse.taubinSmooth(iterations = 20) // volume preserved to within ~1%

Catmull-Clark is absent: it is a quad scheme, TriMesh is triangles-only, and running it on a triangulated quad mesh gives a different (worse) surface than running it on the quads. Loop is the triangle scheme, and it is the one here.

src/jvmMain/kotlin/afield/geometry/mesh/mesh_process.kt — read the source

Used in: Meshes and 3D geometry, Geometry from disk

Top-level functions

Declaration Summary
fun TriMesh.withVertexNormals(areaWeighted: Boolean = true): TriMesh Per-vertex normals from the incident triangles, each weighted by its area.
fun TriMesh.flatShaded(): TriMesh One normal per triangle: every triangle gets its own three vertices, so every edge is hard.
fun TriMesh.shadedNormals(creaseAngleDegrees: Double = 35.0): TriMesh Crease-aware normals: smooth across edges where the two faces are within creaseAngleDegrees of each other, hard where they are not.
fun TriMesh.loopSubdivide(times: Int = 1): TriMesh Loop subdivision, times rounds: every triangle becomes four, and every old vertex moves toward the average of its neighbours by the Loop weights.
fun TriMesh.laplacianSmooth(iterations: Int = 1, lambda: Double = 0.5, cotangent: Boolean = false, preserveBoundary: Boolean = true): TriMesh Laplacian smoothing: each vertex moved lambda of the way toward the weighted average of its neighbours, iterations times.
fun TriMesh.taubinSmooth(iterations: Int = 10, lambda: Double = 0.5, mu: Double = -0.53, cotangent: Boolean = false, preserveBoundary: Boolean = true): TriMesh Taubin λ/μ smoothing: a shrinking pass at +λ followed by an inflating pass at -μ, iterations times.
fun TriMesh.decimate(targetTriangles: Int, maxError: Double? = null): TriMesh Simplify to at most targetTriangles by quadric-error-metric edge collapse.
fun TriMesh.quality(): MeshQuality Measure this mesh's triangle shapes — see MeshQuality.

MeshQuality

data class MeshQuality(val minAngleDegrees: Double, val meanMinAngleDegrees: Double, val meanAspectRatio: Double, val worstAspectRatio: Double, val degenerateCount: Int)

How well-shaped a mesh's triangles are — the number to look at before and after a remesh, a decimation or a marching pass.

Declaration Summary
val minAngleDegrees: Double The smallest interior angle anywhere. 60 degrees is perfectly equilateral; below about 5 the mesh is numerically unpleasant.
val meanMinAngleDegrees: Double Mean over triangles of each triangle's own smallest angle — a better summary than the single worst one.
val meanAspectRatio: Double Mean of circumradius / (2 * inradius): 1 for an equilateral triangle, unbounded for a sliver.
val worstAspectRatio: Double The worst single triangle's aspect ratio.
val degenerateCount: Int Triangles with no area at all. Legal — a UV sphere has none, a marched surface can.
override fun toString(): String All five numbers on one line, for println.

mesh_repair.kt

Why a mesh is not a solid, and what can be done about it — the paved road between "I loaded a file" and toSolid().

TriMesh.diagnose answers why, in terms a person can act on: which loops are open and how big, which edges have the wrong number of faces, which faces are duplicated, whether the winding is consistent and whether it points out. TriMesh.repairToSolid then attempts the fixes in a defensible order and hands back a MeshRepairResult that says what it did and what it could not do — a repair that silently drops half your model is worse than one that refuses, so nothing here returns a bare mesh.

val mesh = meshFromAnyFile("scan.glb")
println(mesh.diagnose().report())     // why it is not a solid, worst first
val fixed = mesh.repairToSolid()
println(fixed.report())               // what was done, and what was not
if (fixed.isSolid) display(fixed.mesh.toSolid() - drill)

What actually blocks toSolid

Every severity below was established by feeding Manifold a mesh damaged in exactly one named way (see MeshDamage in the tests) and asking whether it accepted the result. Three of the answers change how a diagnosis should be read:

  • A mesh with several disjoint components is fine. Manifold accepts it and reports the sum of the volumes. "Split into parts" is not a defect; it is MeshDefectKind.MULTIPLE_COMPONENTS, and it is informational.
  • Zero-area triangles are fine. Manifold accepts them and the volume is unchanged.
  • An inside-out mesh is fine, and that is the dangerous one. Manifold accepts a consistently but inwardly wound closed surface and reports a negative volume — so the boolean you do next silently computes the wrong thing rather than throwing (toSolid() refuses it on Manifold's behalf when the total comes out negative). That is MeshDefectKind.INVERTED_ORIENTATION, and it is the reason this file reports severity MeshDefectSeverity.SUSPICIOUS at all instead of just a pass/fail.

What Manifold genuinely rejects is a short list: a non-finite position, an edge with other than two incident faces (which a duplicated face also produces, on all three of its edges), an open boundary, and inconsistent winding.

Which repair handles which defect

TriMesh.repairToSolid runs TriMesh.repaired first — weld → removeDegenerateTriangles → removeUnusedVertices → fixWinding — which handles an unwelded seam (welding is what turns an STL's pile of triangles into a surface) and inconsistent winding. Of the twelve damaged meshes in MeshRepairTest's table that Manifold rejects, that alone fixes six.

Two families of damage need the repairs in this file:

  1. Open boundaries — fillHoles.
  2. Non-manifold edges — removeDuplicateFaces for a duplicated face, removeNonManifoldFins for a dangling fin.

src/jvmMain/kotlin/afield/geometry/mesh/mesh_repair.kt — read the source

Used in: Solids that cannot fail: Manifold in Field, Geometry from disk, Meshes and 3D geometry

Top-level functions

Declaration Summary
fun TriMesh.diagnose(weldTolerance: Double = 1e-6): MeshDiagnosis Build (and do not cache) this mesh's MeshDiagnosis. See MeshDiagnosis for the vocabulary.
fun TriMesh.removeDuplicateFaces(): TriMesh Remove triangles that share all three corners with another triangle.
fun TriMesh.removeNonManifoldFins(maxRounds: Int = 16): TriMesh Remove dangling flaps: triangles that hang off a non-manifold edge instead of being part of the closed surface.
fun TriMesh.fillHoles(maxEdges: Int = 10_000): HoleFillResult Close every open boundary loop with a triangulated cap.
fun TriMesh.repairToSolid(weldTolerance: Double = 1e-6, fillHoles: Boolean = true, maxHoleEdges: Int = 10_000, keepLargestComponentOnly: Boolean = false): MeshRepairResult Try to turn this mesh into something afield.geometry.solid.manifold.toSolid will accept, and report what it could not fix.

MeshDefectSeverity

enum class MeshDefectSeverity

How much a defect matters — the distinction between "Manifold will refuse this" and "Manifold will accept this and give you a wrong answer without saying so".

Constants: BLOCKING, SUSPICIOUS, INFORMATIONAL.

MeshDefectKind

enum class MeshDefectKind

The kinds of thing that can be wrong with a mesh, declared in the order worth fixing them.

Constants: NON_FINITE_POSITION, UNWELDED_VERTICES, DEGENERATE_FACE, DUPLICATE_FACE, NON_MANIFOLD_EDGE, NON_MANIFOLD_VERTEX, OPEN_BOUNDARY, UNWALKABLE_BOUNDARY, INCONSISTENT_WINDING, INVERTED_ORIENTATION, MULTIPLE_COMPONENTS, UNREFERENCED_VERTEX.

MeshDefect

class MeshDefect(val kind: MeshDefectKind, val severity: MeshDefectSeverity, val count: Int, val detail: String, val fix: String)

One thing that is wrong, with the evidence attached.

Declaration Summary
val kind: MeshDefectKind
val severity: MeshDefectSeverity
val count: Int
val detail: String
val fix: String
override fun toString(): String

BoundaryLoopInfo

class BoundaryLoopInfo(val vertices: List<Int>, val perimeter: Double, val diameter: Double, val planarity: Double, val normal: Vec3)

One open boundary loop, measured.

Declaration Summary
val vertices: List<Int>
val perimeter: Double
val diameter: Double
val planarity: Double
val normal: Vec3
val edgeCount: Int Number of edges around the hole.
val isPlanar: Boolean True when the loop is flat enough that a planar fill is trustworthy.
val isSimple: Boolean True when the ring visits every vertex once.
override fun toString(): String

MeshComponentInfo

class MeshComponentInfo(val triangles: IntArray, val vertexCount: Int, val isClosed: Boolean, val signedVolume: Double, val triangleShare: Double)

One connected piece of the mesh, and whether it is a solid in its own right.

Declaration Summary
val triangles: IntArray
val vertexCount: Int
val isClosed: Boolean
val signedVolume: Double
val triangleShare: Double
override fun toString(): String

MeshDiagnosis

class MeshDiagnosis(val mesh: TriMesh, val weldTolerance: Double = 1e-6)

Everything wrong with one TriMesh, with the evidence, ordered by what to fix first.

Declaration Summary
val topology: MeshTopology The topology this diagnosis reads. Shared, so ask it your own questions rather than rebuilding.
val nonFiniteVertices: List<Int> Vertex indices whose position is NaN or infinite.
val degenerateFaces: List<Int> Triangles with a repeated corner or no area.
val duplicateFaceGroups: List<List<Int>> Groups of two or more triangles sharing the same three corners, whatever their winding.
val nonManifoldEdges: List<Triple<Int, Int, Int>> Edges with three or more incident faces, as (u, v, faceCount).
val nonManifoldVertices: List<Int> Vertices whose incident faces do not form a single fan.
val boundaryLoops: List<BoundaryLoopInfo> Every open boundary loop, measured — see BoundaryLoopInfo. Largest first.
val unwalkableBoundaryEdges: Int Boundary edges that MeshTopology.boundaryLoops could not walk into a closed ring.
val inconsistentEdges: List<Pair<Int, Int>> Interior edges whose two faces walk them the same way — the winding disagreements themselves.
val components: List<MeshComponentInfo> Each connected piece, measured — see MeshComponentInfo. Largest first.
val invertedComponents: List<MeshComponentInfo> Closed components wound inward — accepted by Manifold, and a negative volume when they are.
val weldableVertices: Int How many vertices TriMesh.weld at weldTolerance would merge away.
val unreferencedVertices: Int Vertices no triangle references.
val defects: List<MeshDefect> Every defect found, worst and most causal first.
val blockers: List<MeshDefect> Just the defects Manifold will refuse the mesh over. Empty means TriMesh.toSolid should work.
val isSolid: Boolean True when nothing MeshDefectSeverity.BLOCKING was found.
override fun toString(): String One line: the counts, and the first blocker.
fun report(maxDefects: Int = 20): String The whole diagnosis as readable text, worst first — the thing to print when a toSolid() threw.

HoleFillMethod

enum class HoleFillMethod

How one hole got filled — the difference between a fill you can trust and one you should look at.

Constants: SINGLE_TRIANGLE, EAR_CLIP, CENTROID_FAN, SKIPPED_TOO_LARGE.

HoleFillReport

class HoleFillReport(val loop: BoundaryLoopInfo, val method: HoleFillMethod, val trianglesAdded: Int, val capAreaFraction: Double = 0.0, val note: String? = null)

What happened to one hole.

Declaration Summary
val loop: BoundaryLoopInfo
val method: HoleFillMethod
val trianglesAdded: Int
val capAreaFraction: Double
val note: String?
val isSuspect: Boolean True when this fill is one to look at before trusting: fanned, non-planar, or skipped.
override fun toString(): String

HoleFillResult

class HoleFillResult(val mesh: TriMesh, val fills: List<HoleFillReport>)

The mesh with its holes closed, and the story of each one.

Declaration Summary
val filled: Int Holes that were closed, whatever the method.
val skipped: Int Holes still open because the caller's TriMesh.fillHoles limit refused them.
val suspect: List<HoleFillReport> Fills worth looking at before trusting — see HoleFillReport.isSuspect.

RepairStep

class RepairStep(val name: String, val changed: Int, val detail: String)

One thing a repair pipeline did, and how much of it.

Declaration Summary
override fun toString(): String

MeshRepairResult

class MeshRepairResult(val mesh: TriMesh, val original: TriMesh, val steps: List<RepairStep>, val before: MeshDiagnosis, val after: MeshDiagnosis, val holeFills: List<HoleFillReport>)

What TriMesh.repairToSolid did, what it could not do, and the mesh that came out.

Declaration Summary
val mesh: TriMesh
val original: TriMesh
val steps: List<RepairStep>
val before: MeshDiagnosis
val after: MeshDiagnosis
val holeFills: List<HoleFillReport>
val isSolid: Boolean True when nothing blocking remains — mesh should now survive toSolid().
val unfixed: List<MeshDefect> The blocking defects still present, each carrying its own explanation of why nothing was done.
val remainingWarnings: List<MeshDefect> Defects Manifold will accept but which are probably still wrong — see MeshDefectSeverity.SUSPICIOUS.
val suspectFills: List<HoleFillReport> Fills worth looking at with your eyes before trusting the volume.
override fun toString(): String
fun report(): String The whole story as readable text: what changed, what was filled, and what is still wrong.

mesh_sdf.kt

Modelling with functions: a ScalarField3D is (Vec3) -> Float, the signed-distance primitives and combinators that let you write a shape as an expression, and marchingCubes, which turns any such expression into a watertight TriMesh.

This is the 3D counterpart of contours.kt. There, a ScalarField2D is marched by generateIsoContours into FLine polylines; here a ScalarField3D is marched into triangles. The two files are parallel, and reading the 2D one first is the fastest way to understand this one.

val body = sdfSmoothUnion(
    sdfSphere(0.8),
    sdfBox(vec(1.4, 0.25, 0.25)),
    k = 0.25,
)
val drilled = sdfSubtract(body, sdfCylinder(0.22, 3.0))
val mesh = marchingCubes(drilled, boundsOf(vec(-1.6, -1.6, -1.6), vec(1.6, 1.6, 1.6)), resolution = 96)
println("${mesh.triangleCount} tris, watertight ${mesh.isWatertight()}")

What marchingCubes here actually does

It is a topologically consistent marching cubes, not the 1987 lookup table:

  • The field is sampled once per grid corner, so the cost is (n+1)^3 evaluations however many triangles come out.
  • Every crossing sits on a grid edge, and edges are keyed globally, so a vertex is created once and shared by all the cubes that touch it. That is what makes the output watertight (χ = 2 for a marched sphere) rather than a soup of unwelded triangles needing TriMesh.weld.
  • Instead of a 256-entry triangle table, each cube's crossings are joined into closed loops on the cube's own boundary and each loop is triangulated. On a face where four edges cross — the ambiguous case the classic table resolves arbitrarily — the pairing is chosen by the asymptotic decider (the sign of the bilinear saddle value). Because that decision is a pure function of the face's four corner values, the two cubes sharing the face always agree, which is exactly the property the classic table lacks and the reason plain marching cubes leaks.
  • Winding comes from the field itself: each triangle is oriented against the gradient of the trilinear interpolant, so normals point toward increasing field value — outward, for the signed-distance convention where outside is positive.

Known limits, stated rather than hidden: a cube containing two separate sheets of surface can be oriented from one gradient and get the other sheet backwards, so a field with structure much finer than the grid wants more resolution; and the loop triangulation reproduces the sign pattern faithfully but is not guaranteed to reproduce the trilinear interpolant's exact topology in the rarest configurations.

A note on mutation

Vec3 is mutable and the march reuses one sample vector for speed. A ScalarField3D must therefore read the vector it is handed and not write to it. Every combinator below copies before transforming, so composing them is safe; a hand-written field that does p.mul(2f) will corrupt the march.

src/jvmMain/kotlin/afield/geometry/mesh/mesh_sdf.kt — read the source

Used in: Meshes and 3D geometry, Three.js scenes with three { … }, Typography as geometry — a string becomes contours

Top-level functions

Declaration Summary
fun sampledField3D(values: FloatArray, nx: Int, ny: Int, nz: Int, bounds: AxisAlignedBoundingBox3D, outside: Float = 1e6f): ScalarField3D A grid of samples as a ScalarField3D, read back by trilinear interpolation.
fun boundsOf(a: Vec3, b: Vec3): AxisAlignedBoundingBox3D An AxisAlignedBoundingBox3D from two corners, sorted so min <= max on every axis.
fun boundsAround(center: Vec3 = Vec3(0f, 0f, 0f), radius: Number = 1.0): AxisAlignedBoundingBox3D A cube of side 2 * radius centred on center — the usual march window for a centred shape.
fun TriMesh.bounds(): AxisAlignedBoundingBox3D This mesh's axis-aligned bounds. A unit box centred on the origin for an empty mesh.
fun AxisAlignedBoundingBox3D.expanded(margin: Number): AxisAlignedBoundingBox3D These bounds grown outward by margin on every axis — room for a march not to clip the shape.
fun sdfSphere(radius: Number = 1.0, center: Vec3 = Vec3(0f, 0f, 0f)): ScalarField3D A sphere of radius centred on center. Exact signed distance.
fun sdfBox(size: Vec3 = Vec3(1f, 1f, 1f), center: Vec3 = Vec3(0f, 0f, 0f)): ScalarField3D An axis-aligned box of full extents size centred on center. Exact signed distance (the standard length(max(q,0)) + min(max(q.x,q.y,q.z),0) form, correct outside and inside).
fun sdfRoundBox(size: Vec3 = Vec3(1f, 1f, 1f), radius: Number = 0.1, center: Vec3 = Vec3(0f, 0f, 0f)): ScalarField3D A box with its edges and corners rounded off by radius — sdfBox shrunk then offset.
fun sdfTorus(majorRadius: Number = 1.0, minorRadius: Number = 0.3, center: Vec3 = Vec3(0f, 0f, 0f)): ScalarField3D A torus in the xz plane: majorRadius to the tube centre, minorRadius the tube. Exact.
fun sdfPlane(normal: Vec3 = Vec3(0f, 1f, 0f), offset: Number = 0.0): ScalarField3D A half-space: the signed distance to the plane through normal * offset with that normal.
fun sdfCylinder(radius: Number = 0.5, height: Number = 1.0, center: Vec3 = Vec3(0f, 0f, 0f)): ScalarField3D A finite cylinder along y: radius across, height tall, centred on center. Exact.
fun sdfCapsule(a: Vec3, b: Vec3, radius: Number = 0.25): ScalarField3D A capsule: the radius-thick tube around the segment from a to b. Exact.
fun sdfGyroid(scale: Number = 3.0, thickness: Number = 0.2): ScalarField3D The gyroid, sin x cos y + sin y cos z + sin z cos x, thresholded to a sheet of thickness.
fun sdfMetaballs(centers: List<Vec3>, radius: Number = 0.5, iso: Number = 1.0): ScalarField3D A sum of smooth radial blobs — the classic metaball field, iso at the surface.
fun sdfUnion(vararg fields: ScalarField3D): ScalarField3D Everything inside any of fields — pointwise min.
fun sdfIntersect(vararg fields: ScalarField3D): ScalarField3D Everything inside all of fields — pointwise max.
fun sdfSubtract(a: ScalarField3D, b: ScalarField3D): ScalarField3D a with b drilled out of it — max(a, -b).
fun sdfInvert(f: ScalarField3D): ScalarField3D Inside and outside swapped.
fun sdfSmoothUnion(a: ScalarField3D, b: ScalarField3D, k: Number = 0.2): ScalarField3D A union with a fillet of radius roughly k where the two surfaces meet — the polynomial smooth minimum, the operator that makes SDF modelling feel like clay rather than CSG.
fun sdfSmoothIntersect(a: ScalarField3D, b: ScalarField3D, k: Number = 0.2): ScalarField3D sdfSmoothUnion's counterpart for intersection: a filleted max.
fun sdfSmoothSubtract(a: ScalarField3D, b: ScalarField3D, k: Number = 0.2): ScalarField3D sdfSubtract with a fillet, for a drilled hole whose rim is not a knife edge.
fun sdfExpUnion(fields: List<ScalarField3D>, k: Number = 8.0): ScalarField3D The exponential smooth minimum — smoother than sdfSmoothUnion and, unlike it, associative, so blending many shapes at once gives the same answer whatever order you fold them in.
fun ScalarField3D.shell(thickness: Number = 0.05): ScalarField3D A hollow shell of wall thickness 2 * thickness around this field's surface.
fun ScalarField3D.rounded(radius: Number = 0.05): ScalarField3D This shape grown outward by radius, which rounds off every convex edge it has.
fun ScalarField3D.translated(offset: Vec3): ScalarField3D This field moved by offset.
fun ScalarField3D.scaled(factor: Number): ScalarField3D This field uniformly scaled about the origin. Distances are rescaled too, so it stays an SDF.
fun ScalarField3D.rotated(q: Quat): ScalarField3D This field rotated by q. Rigid, so it stays an SDF.
fun ScalarField3D.repeated(period: Vec3): ScalarField3D This shape tiled infinitely on a lattice of period — one primitive, unlimited copies, at no extra cost per copy. Zero on an axis leaves that axis untiled.
fun ScalarField3D.twisted(turnsPerUnit: Number = 0.25): ScalarField3D This shape twisted about y by turnsPerUnit full turns per unit of height.
fun ScalarField3D.displaced(amplitude: Number = 0.05, frequency: Number = 6.0): ScalarField3D This shape's surface pushed in and out by a sine lattice of amplitude amplitude and spatial frequency — the cheapest interesting texture, and no longer a true distance function (which the marcher does not mind).
fun ScalarField3D.gradient(p: Vec3, h: Number = 1e-3): Vec3 Central-difference gradient of this field at p, step h. Six evaluations.
fun ScalarField3D.normalAt(p: Vec3, h: Number = 1e-3): Vec3 The unit outward normal of this field's iso-surface at p — gradient, normalised.
fun marchingCubes(field: ScalarField3D, bounds: AxisAlignedBoundingBox3D = boundsAround(), resolution: Int = 48, iso: Double = 0.0, positiveOutside: Boolean = true, vertexNormals: Boolean = true): TriMesh The iso level set of field over bounds, as a watertight TriMesh with shared edge vertices.
fun marchingCubes(field: ScalarField3D, bounds: AxisAlignedBoundingBox3D, nx: Int, ny: Int, nz: Int, iso: Double = 0.0, positiveOutside: Boolean = true, vertexNormals: Boolean = true): TriMesh marchingCubes with the cell count given per axis, when the shape wants an anisotropic grid.
fun ScalarField3D.march(bounds: AxisAlignedBoundingBox3D = boundsAround(), resolution: Int = 48, iso: Double = 0.0, positiveOutside: Boolean = true): TriMesh marchingCubes as a method on the field, for sdfSphere(1.0).march(...)-style chains.
fun Img.asHeightSdf(scale: Number = 0.25, bounds: AxisAlignedBoundingBox3D = boundsAround()): ScalarField3D The image as a 3D height field: the image's value at (x, z) minus y, the same for every y. Marching it gives the same surface Img.toHeightfieldMesh builds directly, which is a useful cross-check and occasionally a useful modelling primitive (you can subtract a heightfield from a solid).

ScalarField3D

typealias ScalarField3D = (Vec3) -> Float

A scalar field in space: f(x, y, z). For a signed distance function, negative is inside.

mesh_topology.kt

What a TriMesh is, as opposed to where it is: adjacency, boundaries, components, manifoldness, orientation, and the Euler characteristic those add up to — plus the repairs (TriMesh.weld, TriMesh.fixWinding, TriMesh.removeDegenerateTriangles) that turn a mesh which fails those checks into one that passes.

MeshTopology is a derived, cached view: building one costs one pass over the triangles, and everything after that is a lookup. Build it once and ask it many questions; the convenience properties on TriMesh (TriMesh.isWatertight and friends) build a throwaway one each time, which is fine interactively and wasteful in a loop.

The invariant the tests lean on: for a closed orientable surface, V - E + F = 2 - 2g. So a cube, a sphere and an icosphere all report MeshTopology.eulerCharacteristic 2 and MeshTopology.genus 0, a torus reports 0 and 1, and a two-holed shape reports -2 and 2. A duplicated seam, a missing cap and an unwelded march all change that number, so it is the cheapest single check on a generator.

val t = torusMesh(48, 24)
val topo = t.topology()
println("${topo.eulerCharacteristic} χ, genus ${topo.genus}, watertight ${topo.isWatertight}")
// 0 χ, genus 1, watertight true

src/jvmMain/kotlin/afield/geometry/mesh/mesh_topology.kt — read the source

Used in: Meshes and 3D geometry, Geometry from disk, Solids that cannot fail: Manifold in Field

Top-level functions

Declaration Summary
fun TriMesh.topology(): MeshTopology Build (and do not cache) this mesh's MeshTopology. Hold on to the result if you will ask twice.
fun TriMesh.surfaceTopology(): MeshTopology The topology of the surface rather than of its vertex numbering. A mesh carrying normals, UVs or colours may split a vertex wherever those differ — toTriMesh(normals = true) does at every hard edge — and read raw, every such seam is a line of holes and the genus is wrong. So an attributed mesh has exactly coincident positions welded first. A bare mesh is read as it is, so an unwelded triangle soup still reports its open edges. Figure footers (showMesh, toFigureHtml) use this too.
fun TriMesh.isWatertight(): Boolean True when this mesh is a closed manifold surface — see MeshTopology.isWatertight and surfaceTopology.
fun TriMesh.isManifold(): Boolean True when this mesh is a manifold surface — see MeshTopology.isManifold and surfaceTopology.
fun TriMesh.eulerCharacteristic(): Int V - E + F — see MeshTopology.eulerCharacteristic and surfaceTopology.
fun TriMesh.genus(): Int? Number of handles, or null when the mesh is not an orientable manifold — see MeshTopology.genus. A split-normal mesh is welded by position first, so it reports the genus of its surface.
fun TriMesh.boundaryLoops(): List<List<Int>> Each open border as an afield.geometry.path.FLine-ready ring of vertex indices.
fun TriMesh.weld(tolerance: Double = 1e-6): TriMesh Merge vertices closer together than tolerance and renumber the triangles onto the survivors.
fun TriMesh.removeDegenerateTriangles(areaEpsilon: Double = 1e-14): TriMesh Drop triangles with no area — a repeated index, or three collinear corners — and any vertex that leaves unreferenced.
fun TriMesh.removeUnusedVertices(): TriMesh Drop vertices no triangle references, renumbering the rest.
fun TriMesh.fixWinding(): TriMesh Make the winding consistent across the whole mesh, then make it point outward.
fun TriMesh.repaired(tolerance: Double = 1e-6): TriMesh Weld, drop degenerate triangles, drop unused vertices and fix the winding — the one call to make before trusting a mesh that came from a file, a scanner, or a per-cell generator.
fun TriMesh.largestComponent(): TriMesh The largest connected component, by triangle count — the usual way to throw away the specks a noisy iso-surface leaves around the shape you wanted.

MeshTopology

class MeshTopology(val mesh: TriMesh)

Cached connectivity for one TriMesh.

Declaration Summary
val edges: List<Pair<Int, Int>> Undirected edges as packed (lowIndex, highIndex) pairs, in first-seen order.
val edgeTriangles: List<IntArray> For each entry of edges, the triangles incident on it. Size 1 = boundary, 2 = interior, >2 = non-manifold.
val vertexTriangles: List<IntArray> For each vertex, the triangles that use it.
fun edgeOf(u: Int, v: Int): Int The index into edges of the edge joining u and v, or -1 if there is none.
val edgeCount: Int Number of distinct undirected edges.
val boundaryEdges: List<Pair<Int, Int>> by lazy Edges with exactly one incident triangle — the mesh's open border.
val nonManifoldEdges: List<Pair<Int, Int>> by lazy Edges shared by three or more triangles: the classic non-manifold defect.
val nonManifoldVertices: List<Int> by lazy Vertices whose incident triangles do not form a single fan — two cones meeting at a point, or a bowtie. Legal geometry, illegal for most mesh algorithms: subdivision, decimation and curvature all assume a disc-like neighbourhood.
val isManifold: Boolean True when every edge has one or two triangles and every vertex star is a single fan.
val isWatertight: Boolean True when the mesh is manifold and has no boundary — a closed surface enclosing a volume.
val isConsistentlyOriented: Boolean by lazy True when neighbouring triangles agree about which side is out: every interior edge is traversed in opposite directions by its two triangles.
val boundaryLoops: List<List<Int>> by lazy The open borders, each as a closed ring of vertex indices.
val components: List<List<Int>> by lazy Connected components as vertex-index lists, joined through shared triangles.
val componentCount: Int Number of connected components (ignoring vertices no triangle references).
val eulerCharacteristic: Int V - E + F over the vertices triangles actually use.
val genus: Int? Genus from χ = 2c - 2g - b — the number of handles, 1 for a torus, 2 for a two-holed solid — where c is componentCount and b the number of boundaryLoops.
fun neighbors(v: Int): IntArray The neighbours of v — every vertex sharing an edge with it, in no particular order.
fun isBoundaryVertex(v: Int): Boolean True when v sits on an open boundary.
val boundaryVertices: Set<Int> by lazy Every vertex that sits on an open boundary.
override fun toString(): String One line of the facts most worth printing: counts, χ, genus, and what is broken.

pointing_mesh.kt

Pointing a Ray at a mesh and asking what it hits.

One extension lives here, because the two halves of this question are in different packages. Rays are made in afield.graphics.interaction: Pointer is what a person points with, and the unprojection behind RayCamera turns the pixel or the pose it reports into a Ray. The structure that answers which triangle a ray meets is MeshBvh, which belongs with the meshes and takes an origin and a direction as two separate Vec3 arguments. The overload below unpacks a Ray into them.

Two things follow from that hop and are easier to state here than at the call site. A Ray normalises its direction when it is constructed, so the distance in the RayHit that comes back is in world units however the caller built the ray. And a Ray holds its origin and direction in double precision while Vec3 is float, so both narrow at this call before the intersection test widens them again and runs in double.

For ray to named part rather than ray to triangle, see PickScene.

src/jvmMain/kotlin/afield/geometry/mesh/pointing_mesh.kt — read the source

Top-level functions

Declaration Summary
fun MeshBvh.raycast(ray: Ray, maxDistance: Double = Double.MAX_VALUE, cullBackFaces: Boolean = false): RayHit? Convenience: MeshBvh.raycast down a Ray. Distances come back in world units.

TetgenSolid.kt

src/jvmMain/kotlin/afield/geometry/mesh/tetgen/TetgenSolid.kt — read the source

Top-level functions

Declaration Summary
fun Tetgen.tetrahedralize(solid: Solid, quality: Double = 1.2, targetEdgeLength: Double? = null, preserveSurface: Boolean = false, order: Int = 1): TetMesh Tetrahedralize a Manifold solid straight from its double positions.

GeometryNetwork.kt

The answer Field's geometry-extraction modules share: named points, the curves that join them, and typed channels holding whatever each module measured along the way.

MorseSmale, Polyvector, PrincipalCurves, Mapper, ContourTrees, VectorFieldTopology and the feature tracker all return a GeometryNetwork, so code that filters, counts or draws one does not need to know which of them produced it, and metadata says which did.

The three parts divide the work. GeometryNodes holds the points a module considers meaningful, each under an identity it keeps stable across recomputation where it can, so a downstream object stays attached to the same feature. A GeometryCurve is a polyline that names the nodes at its ends, which is why two curves crossing at a position are still unconnected: connectivity is stated, never inferred from proximity. A Channel packs a scalar, a direction or a covariance for every node or every vertex into one array, so a large network costs no per-element objects.

GeometryNetwork.toFLines turns a network into drawable lines and leaves the network itself on each of them, so whatever the lines cannot carry is still reachable from them.

src/jvmMain/kotlin/afield/geometry/network/GeometryNetwork.kt — read the source

Channel

class Channel(val width: Int, val data: DoubleArray)

A per-element attribute channel: width doubles per element, packed into data.

Declaration Summary
val width: Int Doubles per element: 1 for a scalar, 2 for a 2D tangent, 3 for the xx, xy and yy of a covariance.
val data: DoubleArray Every element's values in storage order; element i begins at i * width.
val count: Int The number of elements this channel describes.
operator fun get(element: Int, component: Int = 0): Double The value at element, or component component of it for a wider channel.

GeometryNodes

class GeometryNodes(val dimension: Int, val ids: IntArray, val positions: DoubleArray, val channels: Map<String, Channel> = emptyMap())

The vertices of a GeometryNetwork: critical points, junctions, endpoints.

Declaration Summary
val dimension: Int Coordinates stored per node, which a network requires its curves to agree with.
val ids: IntArray Node identities in storage order; indexOf maps one back to its index.
val positions: DoubleArray Every node's coordinates in storage order, dimension doubles each.
val channels: Map<String, Channel> Per-node channels by name. Each one describes every node.
val size: Int How many nodes this holds.
fun indexOf(id: Int): Int? The storage index of the node with identity id, or null.
fun position(index: Int): Vec3 The position of the node at storage index index, padded to 3D.

GeometryCurve

class GeometryCurve(val id: Int, val dimension: Int, val coordinates: DoubleArray, val startNode: Int? = null, val endNode: Int? = null, val closed: Boolean = false, val attributes: Map<String, Double> = emptyMap(), val samples: Map<String, Channel> = emptyMap())

One curve of a GeometryNetwork: a polyline in dimension coordinates, its endpoint nodes, and per-vertex samples.

Declaration Summary
val id: Int This curve's identity within its network, which GeometryNetwork.toFLines copies out as curveId.
val dimension: Int Coordinates per vertex: 2 or 3.
val coordinates: DoubleArray The polyline, dimension doubles per vertex, in order along the curve.
val startNode: Int? The identity of the node this curve starts at, or null when it starts at no node.
val endNode: Int? The identity of the node this curve ends at, or null when it ends at no node.
val closed: Boolean Whether the last vertex joins back to the first.
val attributes: Map<String, Double> Whole-curve values by name, such as kind or born, each copied onto the line this becomes.
val samples: Map<String, Channel> Per-vertex channels by name. Each one holds a value for every vertex of this curve.
val vertexCount: Int How many vertices the polyline has.
fun vertex(i: Int): Vec3 Vertex i, padded to 3D.
val length: Double Polyline length.

GeometryNetwork

class GeometryNetwork(val dimension: Int, val nodes: GeometryNodes, val curves: List<GeometryCurve>, val metadata: Map<String, String> = emptyMap())

The common result type of Field's geometry-extraction modules (Morse–Smale, polyvector, principal curves): explicit nodes, curves that name their endpoint nodes, typed attribute channels, and string metadata recording provenance (which module, which parameters).

Declaration Summary
val dimension: Int Coordinates per point. Every node and every curve must agree with it.
val nodes: GeometryNodes The points curves may name as their ends.
val curves: List<GeometryCurve> The curves, which may name only nodes that are in nodes.
val metadata: Map<String, String> Provenance by name: which module made this network, and under which parameters.
fun curvesAt(nodeId: Int): List<GeometryCurve> Curves whose start or end is the node with identity nodeId.
fun degrees(): Map<Int, Int> The number of curve ends meeting at each node identity.
fun components(): List<List<Int>> Connected components, as lists of curve ids. Curves are connected only through shared nodes, never by proximity.
fun filterCurves(keep: (GeometryCurve) -> Boolean): GeometryNetwork A network holding only the curves that satisfy keep, and the nodes they still name.
fun toFLines(smooth: Boolean = false, tension: Float = 1f, withSamples: Boolean = false): List<FLine> One FLine per curve.
fun nodesFLine(): FLine Every node as a single FLine of points, for display; each point carries nodeId and node channels.
override fun toString() Dimension, node and curve counts, and the source recorded in metadata, on one line.

NativeGeometry.kt

Which of Field's optional native geometry libraries are installed, and what the welcome page says about the rest.

Morse–Smale complexes, principal curves and polyvector vectorization each need a native library that Field does not build by default. NativeGeometry holds one NativeGeometryCapability per module, registered by that module's own library object, and answers NativeGeometry.available and NativeGeometry.describe without loading any of them, so asking never fails. Field runs without any of them, and calling into one that is absent throws an error naming the Gradle task that builds it.

NativeGeometry.status reports every module as a NativeGeometryStatus, which GET /api/native-geometry-status serves to the welcome page's Geometry extensions panel: whether the library loaded, its docs page, its build task, and the shell commands that install its prerequisites on macOS, Linux and Windows.

src/jvmMain/kotlin/afield/geometry/network/NativeGeometry.kt — read the source

NativeGeometryCapability

interface NativeGeometryCapability

One optional native geometry library, as NativeGeometry sees it. Implementations must answer without loading the library's symbols, so asking never fails.

Declaration Summary
val name: String
val isAvailable: Boolean
fun describe(): String One line: where the library was found, or why not and how to build it.
val title: String A short human title, for the welcome page.
val docsPage: String The docs page, relative to the docs root (e.g. morse_smale.md).
val buildTask: String The Gradle task that builds it.
val install: Map<String, List<String>> Shell commands that install its prerequisites and build it, per OS (macos, linux, windows). An empty list for an OS means the build does not support it yet.

NativeGeometryStatus

data class NativeGeometryStatus(val name: String, val title: String, val available: Boolean, val description: String, val docsPage: String, val buildTask: String, val install: Map<String, List<String>>)

One library's state, as GET /api/native-geometry-status reports it to the welcome page.

Declaration Summary
val name: String
val title: String
val available: Boolean
val description: String
val docsPage: String
val buildTask: String
val install: Map<String, List<String>>

NativeGeometry

object NativeGeometry

Which of Field's optional geometry libraries are installed.

Declaration Summary
fun register(name: String, capability: () -> NativeGeometryCapability) Called by each module's library object; the factory is evaluated only when asked.
val names: Set<String> Module names Field knows about, installed or not.
fun available(name: String): Boolean
fun describe(name: String): String
fun describe(): String
fun status(): List<NativeGeometryStatus> Every module's state, for the welcome page. Never throws: a broken library describes itself.

FLine.kt

FLine is Field's drawn path: a list of move, line and cubic segments with per-node and per-line properties, built with moveTo / lineTo / cubicTo and drawn by Box.lines(...).

Also here are the operators that translate, scale and rotate a List<FLine> (or a map of them) as a group.

src/jvmMain/kotlin/afield/geometry/path/FLine.kt — read the source

Top-level functions

Declaration Summary
operator fun List<FLine>.plus(offset: Vec3)+1 overload · also Vec2 Translate all lines in the list by offset.
operator fun List<FLine>.times(scale: Vec3) Scale all lines in the list component-wise by scale.
operator fun List<FLine>.times(scale: Vec2) Scale all lines in the list component-wise by scale.
operator fun List<FLine>.times(scale: Number) Uniformly scale all lines in the list by scale.
operator fun List<FLine>.times(rotation: Quat) Rotate all lines in the list by rotation.
operator fun Map<String, List<FLine>>.plus(offset: Vec3) Translate all lines in each map entry by offset.
operator fun FLineMap.plus(offset: Vec3) Translate all lines in an FLineMap by offset.
operator fun Map<String, List<FLine>>.times(offset: Vec3) Scale all lines in each map entry component-wise by offset.
operator fun FLineMap.times(offset: Vec3) Scale all lines in an FLineMap component-wise by offset.
operator fun Map<String, List<FLine>>.times(scale: Number) Uniformly scale all lines in each map entry by scale.
operator fun FLineMap.times(scale: Number) Uniformly scale all lines in an FLineMap by scale.
operator fun Map<String, List<FLine>>.times(rotation: Quat) Rotate all lines in each map entry by rotation.
operator fun FLineMap.times(rotation: Quat) Rotate all lines in an FLineMap by rotation.

Top-level values

Declaration Summary
var _flineSequenceNumber

FLine

class FLine : HasProperties

Mutable path composed of move/line/cubic/close instructions.

Declaration Summary
var nodes
val properties
var rev
val uid
sealed class Node(val n: Int) : HasProperties Base class for drawing instructions in an FLine.
val here: Node The most recently added node.
fun endPointOf(index: Int): Vec3 Where the instruction at index leaves the pen.
val currentPoint: Vec3? Where the pen sits after the last instruction — the point a lineTo/cubicTo would start from — or null for a line with no instructions at all.
class MoveTo : Node(1) Move instruction (starts a new subpath).
class LineTo : Node(1) Line instruction (straight segment).
class CubicTo : Node(3) Cubic Bezier instruction (endpoint plus two control points).
class Close : Node(0) Close instruction (connects back to the most recent move).
var breakNext When set, the next lineTo or cubicTo becomes a moveTo.
fun breakNext() Convenience to set breakNext for the next instruction.
fun moveTo(x: Number, y: Number, z: Number = 0): FLine Move the current point without drawing, starting a new subpath.
fun moveTo(v: Vec3): FLine Move the current point without drawing, starting a new subpath.
fun moveTo(v: Vec2): FLine 2D overload of moveTo.
fun moveTo(v: Vector3d): FLine 3D vector overload of moveTo.
fun lineTo(x: Number, y: Number, z: Number = 0): FLine Add a line segment to the given position.
fun lineTo(v: Vec3): FLine Add a line segment to the given position.
fun lineTo(v: Vector3d): FLine 3D vector overload of lineTo.
fun lineTo(v: Vec2): FLine 2D overload of lineTo.
fun cubicTo(x0: Number, y0: Number, z0: Number = 0, x1: Number, y1: Number, z1: Number = 0, x: Number, y: Number, z: Number = 0): FLine Add a cubic Bezier segment.
fun byThickening(_thickness: Float) Return a new line thickened to the given stroke width.
fun byFiltering(predicate: (Vec3?, Node) -> Boolean): FLine Return a filtered copy of this line.
fun cubicTo(x0: Number, y0: Number, x1: Number, y1: Number, x: Number, y: Number): FLine 2D overload of cubicTo (z defaults to 0).
fun cubicTo(v0: Vec3, v1: Vec3, v: Vec3): FLine Add a cubic Bezier segment using vector control points.
fun close(): FLine Close the current subpath.
fun transform(by: (Vec3) -> Vec3): FLine Copy this line while transforming every stored point.
fun rot(angle: Number, center: FractionAlongAnchor): FLine+3 overloads · also Vec2, Vec3, ListAnchor Rotate in the XY plane around center, preserving Z.
fun rot(angle: Number): FLine Rotate around the Z axis and the line center by default.
fun rot(angle: Number, axis: Vec3, center: Vec3 = center()): FLine+2 overloads · also ListAnchor, FractionAlongAnchor Rotate in 3D around axis and center.
fun scale(scale: Number): FLine Uniform scale around the line center by default.
fun scale(scale: Number, center: Vec2): FLine
fun scale(scale: Number, center: Vec3): FLine
fun scale(scale: Number, center: ListAnchor): FLine
fun scale(scale: Number, center: FractionAlongAnchor): FLine
fun scale(scale: Vec2, center: Vec2 = anchor2(ListAnchor.Center)): FLine XY component scale around center, preserving Z.
fun scale(scale: Vec2, center: ListAnchor): FLine
fun scale(scale: Vec2, center: FractionAlongAnchor): FLine
fun scale(scale: Vec3, center: Vec3 = center()): FLine XYZ component scale around center.
fun scale(scale: Vec3, center: ListAnchor): FLine
fun scale(scale: Vec3, center: FractionAlongAnchor): FLine
operator fun plus(n: Vec3): FLine Translate this line by n.
operator fun minus(n: Vec3): FLine Translate this line by -n.
operator fun times(n: Vec3): FLine Scale this line component-wise by n.
operator fun times(n: Quat): FLine Rotate this line by n.
operator fun times(n: Number): FLine Uniformly scale this line by n.
operator fun plus(n: Vec2) 2D translation overload of plus.
operator fun minus(n: Vec2) 2D translation overload of minus.
operator fun times(n: Vec2) 2D scale overload of times.
fun copyTo(other: Node) Append a copy of other to this line.
fun copy() Deep copy of this line and its properties.
fun center(): Vector3f
fun midPoint(): Vector3f
fun closeMidPoint()
fun copy(block: FLine.() -> Unit) Copy this line and apply a mutating block to the clone.
fun cursor(tol: Float = 0.01f): FLinesAndJavaShapes.Cursor Create a cursor for querying length, tangents, normals, and positions.
fun cursor(closestTo: Vec3): Cursor Create a cursor positioned at the closest point to closestTo.
fun sampleByNumber(steps: Int): List<Vec3> Sample evenly along arc length.
fun drop(n: Int = 1): FLine Remove the last n nodes from this line.
fun sampleByDistance(distance: Float): List<Vec3> Sample points by stepping along the curve in fixed increments.
override fun setPropertyRaw(name: String, value: Any, doResync: Boolean)
override fun getPropertyRaw(name: String, default: Any?): Any?
override fun removePropertyRaw(name: String, doResync: Boolean)
override fun toString(): String
fun pieces(): List<FLine> Split this line into subpaths at MoveTo boundaries.
fun fast2DBounds(): Rect Fast axis-aligned 2D bounds based on node control points.
fun fast3DBounds(): Pair<Vec3, Vec3>
fun data(format: String, parameters: List<Any>): FLine Build commands from a compact format string and parameter list.
fun data(format: String, vararg parameters: Any): FLine Variadic form of data. Lets you write f.data("m l l z", 0,0, 1,0, 1,1) instead of building a List<Any> by hand. Parameters may be a mix of Number runs, Vec2 and Vec3; see data for the format grammar (m move, l line, c cubic, s smooth, z close, ( ) groups, trailing + greedy repeat).
fun uniform_linearize(samplesPerCubic: Int = 100, a: (Vec3, Float) -> Unit) Walk the path into line segments. Callback receives: - position: current point - instr: drawing-instruction index for this point. A negative value indicates the start of a subpath (equivalent to the old break flag); otherwise it's interpolated between nodes (e.g. along cubics).
fun uniform_linearize_cubic(dest: (Vec3, Float) -> Unit, a: Vec3, c1: Vec3, c2: Vec3, b: Vec3, samplesPerCubic: Int = 100, storage: Vec3, fromInstr: Double, toInstr: Double) Sample a cubic Bezier segment and feed points to dest.
fun invoke(f: FLine.() -> FLine): FLine+1 overload · also FLine.() -> Unit
fun tessellate(samplesPerCubic: Int = 100, coplanarity: Coplanarity = Coplanarity.DEFAULT, meshAcceptor: (FloatArray, List<Int>) -> Unit) Tessellate filled subpaths into triangle indices.
operator fun plusAssign(other: FLine)
operator fun plusAssign(p: Vec3) Append a point as a lineTo (or a moveTo if this line is empty): f += vec(1, 2).
operator fun plusAssign(p: Vec2) 2D overload of plusAssign.
operator fun Vec3.unaryMinus(): FLine Draw a segment to this point (the dash looks like a segment). Starts the path if empty.
operator fun Vec2.unaryMinus(): FLine 2D overload of the draw operator.
operator fun Vec3.unaryPlus(): FLine Lift the pen and start a new subpath at this point.
operator fun Vec2.unaryPlus(): FLine 2D overload of the pen-up operator.
fun colorsPer(): FloatArray One Vec4 color per drawing instruction. Resolution order: node.properties["color"] if Vec4 → previous color → fline.properties["color"] if Vec4 → default vec4(0.8,0.8,0.8,0.5)
fun pointSizePer(): FloatArray One point size (float) per drawing instruction. Resolution order: node.properties["pointSize"] if Number → previous size → fline.properties["pointSize"] if Number → default 1.0f
fun clear() Remove all of the contents of this line

FLineEditing.kt

The server side of an editable Box.lines(..., editable = true) drawing: an edit session per widget that applies the browser's point drags and new strokes to the live FLines.

Box.lines returns an EditableFLineViewState when editable = true (null otherwise); call EditableFLineViewState.currentLines to read the lines as they are after the user's edits.

src/jvmMain/kotlin/afield/geometry/path/FLineEditing.kt — read the source

EditableFLineViewState

class EditableFLineViewState internal constructor(val sessionId: String)

Declaration Summary
val sessionId: String
fun currentLines(): List<FLine>

FLineExact.kt

Curve-preserving boolean operations and cubic offsets for FLine, in the manner of Paper.js.

Booleans: every segment of both operands is cut at its exact crossings with every other segment (Bézier–Bézier by bounding-box subdivision refined by Newton, plus endpoint-on-curve projection for coincident edges and T-junctions). Each resulting piece is a sub-curve of an original segment, so cubics stay cubics and straight segments stay FLine.LineTo. A piece is kept when the operation's answer differs a hair to its left and to its right (winding numbers of both operands, non-zero rule), oriented with the result on its left, and the kept pieces are chained into closed contours. The result is checked against the java.awt.geom.Area answer by area; on any failure the Area answer is returned instead (FLineBooleanResult.exact says which).

Offsets: each cubic is approximated by a cubic whose end tangents are the original's and whose handle lengths are fitted to the true offset at the midpoint (Tiller–Hanson curvature scaling where that fit is ill-conditioned), subdivided until the sampled error is within tolerance. Corners get round, miter or bevel joins; closed results are resolved through the boolean engine with a positive-winding rule, which removes the swallowtails at concave corners.

All arithmetic is in doubles over the xy plane; z is dropped.

src/jvmMain/kotlin/afield/geometry/path/FLineExact.kt — read the source

Used in: FLine examples (Field graphics)

Top-level functions

Declaration Summary
fun FLine.booleanExact(other: FLine, op: FLineBooleanOp, stroke: Boolean = false): FLineBooleanResult Boolean op of this filled region with other, keeping the curves: the result's cubics are pieces of the operands' cubics, cut exactly where the outlines cross. Falls back to the java.awt.geom.Area answer when the exact engine cannot produce a result it trusts (see FLineBooleanResult.fallbackReason). Both lines are read as filled, non-zero, in the xy plane, whatever their thickness or thicken; stroke reads a styled line as its stroke outline instead (through Area).
fun FLine.byOffsettingExact(distance: Float, tolerance: Float = 0.01f, join: FLineStrokeJoin = FLineStrokeJoin.ROUND, miterLimit: Float = 4f): FLine Offset this line by distance with cubic output: each cubic becomes a few cubics within tolerance of the true offset (rather than byOffsetting's sampled polyline), straight segments stay straight, and corners get join (a miter longer than miterLimit × distance becomes a bevel). Positive distance is towards FLineLocation.normal2D (left of travel on Field's y-down canvas), as in byOffsetting. Closed subpaths are resolved into clean outlines (concave-corner swallowtails and collapsed parts removed); open ones are returned as single offset runs.
fun FLine.segmentCount2D(): Int number of drawn, non-degenerate segments (what the exactness tests count).

FLineBooleanOp

enum class FLineBooleanOp

The four boolean operations.

Constants: Union, Intersect, Subtract, Xor.

FLineBooleanResult

class FLineBooleanResult(val line: FLine, val exact: Boolean, val fallbackReason: String?)

The answer of FLine.booleanExact: line is the result, exact is whether it came from the curve-preserving engine (otherwise from java.awt.geom.Area, for the fallbackReason given).

FLineGaussianProcess.kt

Gaussian-process curves: fit a GP to an FLine or to points, then draw its mean, samples and uncertainty as lines.

FLine.gaussianProcess and gpCurve return an FLineGaussianProcess, which gives the mean, covariance, velocity and moving frames along the curve; FLineGaussianProcess.posterior gives an FLineGpJoint for coherent samples. For scalar GPs, meanFLine, sampleFLine and intervalFLines turn a joint into plottable lines. Tests: FLineGaussianProcessTest, FLineGpGeometryTest.

src/jvmMain/kotlin/afield/geometry/path/FLineGaussianProcess.kt — read the source

Used in: Drawing with Gaussian processes

Top-level functions

Declaration Summary
fun gpCurve(parameters: DoubleArray, points: List<Vec3>, kernel: GpKernel = GpKernels.rbf(0.15), noiseStd: DoubleArray = DoubleArray(points.size), dimensions: Int = 3, mean: Vec3? = null, outputCovariance: Array<DoubleArray> = Array(dimensions) { i -> DoubleArray(dimensions) { j -> if (i == j) 1.0 else 0.0 } }, numerics: GpNumerics = GpNumerics(), velocities: List<Pair<Double, Vec3>> = emptyList(), velocityNoiseStd: Double = 0.0): FLineGaussianProcess Fit explicitly parameterised curve observations. Defaults to a constant mean at their centroid. A 2D curve must have constant Z, retained in every result. Output covariance is dimensionless; kernel variance supplies the squared coordinate units. Supply a periodic kernel for a closed loop. velocities pins d(position)/dt at chosen parameters (coordinate units per unit parameter), e.g. to make a curve leave a point in a given direction; it needs a GpDifferentiableKernel.
fun FLine.gaussianProcess(trainingSamples: Int = 48, kernel: GpKernel = GpKernels.rbf(0.15), noiseStd: Double = 0.0, dimensions: Int = 3, mean: Vec3? = null, outputCovariance: Array<DoubleArray> = Array(dimensions) { i -> DoubleArray(dimensions) { j -> if (i == j) 1.0 else 0.0 } }, sampler: FLineSampler? = null, numerics: GpNumerics = GpNumerics()): FLineGaussianProcess Fit one continuous path at normalised arc length, retaining posterior uncertainty for later use.
fun GpJoint.meanFLine(xs: DoubleArray): FLine A scalar joint drawn as y(x), useful for 1D means, bands and samples.
fun GpJoint.sampleFLine(xs: DoubleArray, seed: Long): FLine
fun GpJoint.sampleFLine(xs: DoubleArray, seed: Long, spread: Double): FLine
fun GpJoint.intervalFLines(xs: DoubleArray, standardDeviations: Double = 1.96): Pair<FLine, FLine>

FLineGaussianProcess

class FLineGaussianProcess internal constructor(val model: GpVectorProcess, private val planeZ: Double)

A 2D/3D curve as a vector-valued GP on its scalar parameter, with full output covariance. Velocities, frames and curvature need a GpDifferentiableKernel (the default RBF is one).

Declaration Summary
val dimensions: Int
fun mean(t: Double): Vec3
fun variance(t: Double): Vec3 Coordinate variances; these are squared distances, not standard deviations or a ribbon width.
fun covariance(t: Double, s: Double = t): Array<DoubleArray> Full 3×3 covariance block; planar curves have a zero Z row and column.
fun posterior(parameters: DoubleArray): FLineGpJoint
fun velocity(t: Double): Vec3 Mean velocity d(position)/dt, in coordinate units per unit of parameter (Z is 0 for a planar curve).
fun velocities(parameters: DoubleArray): GpVectorJoint The joint velocity at every parameter; sample it for coherent tangent fields.
fun frames(parameters: DoubleArray, step: Double = defaultStep(parameters)): List<GpCurveFrame> A moving frame along the mean curve, with how uncertain its tangent is. The normal of a planar curve is the tangent turned a quarter to the left; a spatial curve's is the Frenet normal, taken from the mean velocity step either side, and is null where the curve is straight.
fun curvature(parameters: DoubleArray, samples: Int = 256, seed: Long = 0L, step: Double = defaultStep(parameters)): GpCurvature Curvature with uncertainty, by drawing samples whole velocity fields and differencing each step either side. GpCurvature.ofMean is the curvature of the mean curve, which is not the mean curvature of the draws: wiggly draws curve more than their average does. Planar curvature is signed (positive turns left); spatial curvature is unsigned. Costs a joint of 3 × parameters × dimensions values, so keep parameters to a few hundred. step defaults to a thousandth of the parameter span (at least 0.001); much smaller and the sampler can no longer resolve the difference between neighbouring velocities, understating the uncertainty. The same floor applies to a curve pinned so tightly that its velocity variance nears the GpNumerics tolerances; if GpCurvature.std changes with step, it is at that floor.

GpCurveFrame

data class GpCurveFrame(val parameter: Double, val position: Vec3, val tangent: Vec3, val normal: Vec3?, val binormal: Vec3?, val speed: Double, val speedStd: Double, val tangentAngleStd: Double)

One frame of FLineGaussianProcess.frames. speed is in coordinate units per unit parameter; tangentAngleStd is the delta-method standard deviation of the tangent's direction, in radians.

Declaration Summary
val parameter: Double, val position: Vec3, val tangent: Vec3, val normal: Vec3?, val binormal: Vec3?
val speed: Double, val speedStd: Double, val tangentAngleStd: Double

GpCurvature

class GpCurvature internal constructor(parameters: DoubleArray, ofMean: DoubleArray, mean: DoubleArray, std: DoubleArray)

Curvature in 1 / coordinate units. See FLineGaussianProcess.curvature.

Declaration Summary
val parameters: DoubleArray
val ofMean: DoubleArray
val mean: DoubleArray
val std: DoubleArray

FLineGpJoint

class FLineGpJoint internal constructor(private val ts: DoubleArray, val distribution: GpVectorJoint, private val planeZ: Double)

Draws and uncertainty on the same parameter grid. All vertices of a draw are correlated.

Declaration Summary
val parameters: DoubleArray
val meanPoints: List<Vec3>
val variances: List<Vec3>
fun meanLine(): FLine
fun samplePoints(seed: Long): List<Vec3>
fun sampleLine(seed: Long): FLine
fun sampleLine(seed: Long, spread: Double): FLine spread scales each vertex's departure from the mean: 0 is the mean line, 1 a draw at the fitted scale, above 1 an exaggerated one.
fun covarianceAt(index: Int, other: Int = index): Array<DoubleArray>
fun projectedVariance(index: Int, direction: Vec3): Double Pointwise variance in a chosen direction; handles correlated XYZ, not just diagonal terms.
fun normalBand(standardDeviations: Double = 1.96): Pair<FLine, FLine> Two pointwise normal-offset curves around a planar mean. Not a simultaneous credible region or the envelope of all draws. Degenerate tangents use the positive Y direction.

FLineGeometry.kt

Comprehensive computational geometry utilities for FLine operations implemented as extension functions. Provides transformations, projections, intersections, decompositions, curvature analyses, tangents, osculating planes, offsets, and a rich set of 2D and 3D geometric algorithms.

Functions that return new FLine instances use a "by" prefix to indicate non-mutating operations: - byWaving() instead of wave() - byOffsetting() instead of offset()

src/jvmMain/kotlin/afield/geometry/path/FLineGeometry.kt — read the source

Used in: FLine examples (Field graphics), Vectorizing line drawings: polyvector fields, Euclid's Elements

Top-level functions

Declaration Summary
fun fractionAlong(t: Number)
operator fun List<Vec2>.plus(s: Number): List<Vec2>
operator fun List<Vec2>.minus(s: Number): List<Vec2>
operator fun List<Vec2>.times(s: Number): List<Vec2>
operator fun List<Vec2>.div(s: Number): List<Vec2>
operator fun List<Vec2>.plus(v: List<Float>): List<Vec2>+2 overloads · also Vec2, List<Vec2>
operator fun List<Vec2>.minus(v: List<Float>): List<Vec2>+2 overloads · also Vec2, List<Vec2>
operator fun List<Vec2>.times(v: List<Float>): List<Vec2>+2 overloads · also Vec2, List<Vec2>
operator fun List<Vec2>.div(v: List<Float>): List<Vec2>+2 overloads · also Vec2, List<Vec2>
operator fun List<Vec2>.plus(v: List<Vec3>): List<Vec3>+1 overload · also Vec3
operator fun List<Vec2>.minus(v: List<Vec3>): List<Vec3>+1 overload · also Vec3
operator fun List<Vec2>.times(v: List<Vec3>): List<Vec3>+1 overload · also Vec3
operator fun List<Vec2>.div(v: List<Vec3>): List<Vec3>+1 overload · also Vec3
operator fun Number.plus(v: List<Vec2>): List<Vec2>
operator fun Number.minus(v: List<Vec2>): List<Vec2>
operator fun Number.times(v: List<Vec2>): List<Vec2>
operator fun Number.div(v: List<Vec2>): List<Vec2>
operator fun List<Vec3>.plus(s: Number): List<Vec3>
operator fun List<Vec3>.minus(s: Number): List<Vec3>
operator fun List<Vec3>.times(s: Number): List<Vec3>
operator fun List<Vec3>.div(s: Number): List<Vec3>
operator fun List<Vec3>.plus(v: List<Float>): List<Vec3>+4 overloads · also Vec3, Vec2, List<Vec3>, List<Vec2>
operator fun List<Vec3>.minus(v: List<Float>): List<Vec3>+4 overloads · also Vec3, Vec2, List<Vec3>, List<Vec2>
operator fun List<Vec3>.times(v: List<Float>): List<Vec3>+4 overloads · also Vec3, Vec2, List<Vec3>, List<Vec2>
operator fun List<Vec3>.div(v: List<Float>): List<Vec3>+4 overloads · also Vec3, Vec2, List<Vec3>, List<Vec2>
operator fun Number.plus(v: List<Vec3>): List<Vec3>
operator fun Number.minus(v: List<Vec3>): List<Vec3>
operator fun Number.times(v: List<Vec3>): List<Vec3>
operator fun Number.div(v: List<Vec3>): List<Vec3>
infix fun List<Vec2>.dot(v: List<Vec2>): List<Float>+1 overload · also Vec2
infix fun List<Vec3>.dot(v: List<Vec3>): List<Float>+1 overload · also Vec3
infix fun List<Vec2>.cross(v: List<Vec2>): List<Float>+1 overload · also Vec2
infix fun List<Vec3>.cross(v: List<Vec3>): List<Vec3>+1 overload · also Vec3
fun List<Vec2>.perpendicular(): List<Vec2>
fun List<Vec2>.perp(): List<Vec2>
fun List<Vec3>.perpendicular(): List<Vec3>
fun List<Vec3>.perp(): List<Vec3>
fun List<Vec2>.rot(angle: Number, center: Vec2Anchor = Anchors2.center): List<Vec2>
fun List<Vec2>.rot(angle: Number, about: FractionAlongAnchor): List<Vec2>+2 overloads · also Vec2, ListAnchor
fun List<Vec3>.rot(angle: Number, axis: Vec3 = vec(0f, 0f, 1f), center: Vec3Anchor = Anchors3.center): List<Vec3>
fun List<Vec3>.rot(angle: Number, center: Vec3Anchor): List<Vec3>
fun List<Vec3>.rot(angle: Number, axis: Vec3 = vec(0f, 0f, 1f), about: Vec3): List<Vec3>+2 overloads · also ListAnchor, FractionAlongAnchor
fun List<Vec3>.rot(angle: Number, about: FractionAlongAnchor): List<Vec3>+1 overload · also ListAnchor
fun List<Vec2>.scale(scale: Number, center: Vec2Anchor = Anchors2.center): List<Vec2>+1 overload · also Vec2
fun List<Vec2>.scale(scale: Number, about: FractionAlongAnchor): List<Vec2>+5 overloads · also Vec2, ListAnchor
fun List<Vec3>.scale(scale: Number, center: Vec3Anchor = Anchors3.center): List<Vec3>+1 overload · also Vec3
fun List<Vec3>.scale(scale: Number, about: FractionAlongAnchor): List<Vec3>+5 overloads · also Vec3, ListAnchor
fun FLine.byRotating2D(angle: Float, center: Vec2 = vec(0f, 0f)): FLine Rotate an FLine around a point in 2D (preserves Z coordinate).
fun FLine.byScaling2D(scale: Float, center: Vec2 = vec(0f, 0f)): FLine Scale an FLine from a center point.
fun FLine.byScaling2D(scaleX: Float, scaleY: Float, center: Vec2 = vec(0f, 0f)): FLine Scale an FLine with different factors for X and Y.
fun FLine.byReflecting2D(linePoint1: Vec2, linePoint2: Vec2): FLine Reflect an FLine across a line defined by two points.
fun FLine.byShearing2D(shearX: Float, shearY: Float): FLine Shear transformation in 2D.
fun FLine.byRotating3D(angle: Float, axis: Vec3, center: Vec3 = vec(0f, 0f, 0f)): FLine Rotate an FLine around an arbitrary 3D axis.
fun FLine.byPerspectiveProjecting(cameraPos: Vec3, planePoint: Vec3 = vec(0f, 0f, 0f), planeNormal: Vec3 = vec(0f, 0f, 1f)): FLine Project this FLine onto a plane with perspective.
fun FLine.byOrthographicProjecting(planePoint: Vec3 = vec(0f, 0f, 0f), planeNormal: Vec3 = vec(0f, 0f, 1f)): FLine Project this FLine onto a plane orthographically.
fun FLine.byOffsetting(distance: Float, samples: Int = 50): FLine Create an offset curve at a specified distance from the original. Uses normal vectors to offset the curve. For 2D curves, the offset is perpendicular to the curve.
fun FLine.byCreatingParallelCurves(distance: Float, samples: Int = 50): Pair<FLine, FLine> Create parallel curves on both sides of the original curve.
fun FLine.byGaussianSmoothing(sigma: Float, samples: Int = 100, sampler: FLineSampler? = null): FLine Smooth a curve using Gaussian kernel smoothing.
fun FLine.bySimplifying(epsilon: Float): FLine Simplify a curve using the Ramer-Douglas-Peucker algorithm.
fun FLine.curvatureAnalysis(samples: Int = 100): List<Pair<Vec3, Float>> Calculate curvature at regular intervals along the curve.
fun FLine.byCurvatureAdaptiveSampling(minSamples: Int = 20, maxSamples: Int = 200): FLine Sample the curve with density proportional to curvature. High curvature areas get more samples.
fun FLine.findHighCurvaturePoints(threshold: Float = 0.1f, samples: Int = 100): List<Vec3> Find points of maximum curvature.
fun FLine.byConvexHull2D(): FLine Compute the 2D convex hull of the curve using Graham scan.
fun FLine.bySplittingAtInflections(): List<FLine> Split curve at points of inflection (where curvature changes sign).
fun FLine.byCreatingTangentLines(samples: Int = 20, length: Float = 10f): List<FLine> Generate tangent lines at regular intervals.
fun FLine.byCreatingNormalLines(samples: Int = 20, length: Float = 10f): List<FLine> Generate normal lines at regular intervals.
fun FLine.byCreatingOsculatingCircles(samples: Int = 20, circleSegments: Int = 32): List<FLine> Generate osculating circles at points along the curve. The osculating circle has the same tangent and curvature as the curve at that point.
fun FLine.byUnionWith(other: FLine, exact: Boolean = false, stroke: Boolean = false): FLine Union of two curves (using Java2D Area).
fun FLine.byDifferenceWith(other: FLine, exact: Boolean = false, stroke: Boolean = false): FLine Difference of two curves (line1 - line2).
fun FLine.byIntersectionWith(other: FLine, exact: Boolean = false, stroke: Boolean = false): FLine Intersection of two curves.
fun FLine.byExclusiveOrWith(other: FLine, exact: Boolean = false, stroke: Boolean = false): FLine XOR (exclusive or) of two curves.
fun FLine.centroid2D(): Vec2 Calculate the centroid of a 2D closed curve.
fun FLine.byReversing(): FLine Reverse the direction of a curve.
fun concatenate(vararg lines: FLine): FLine Concatenate multiple curves into one.
fun FLine.byBlendingWith(other: FLine, t: Float, sampler: FLineSampler? = null, otherSampler: FLineSampler? = sampler): FLine Blend between two curves.
fun FLine.byExtracting(tStart: Float, tEnd: Float): FLine Extract a sub-curve between two t parameters.
fun FLine.fitCircle(samples: Int = 20, sampler: FLineSampler? = null): Triple<Vec2, Float, Float> Fit a circle to a set of points on the curve.
fun FLine.fitLine2D(samples: Int = 20, sampler: FLineSampler? = null): Triple<Vec2, Vec2, Float> Fit a line to points on the curve using least squares in 2D. Returns the line as two points (start and end) that best fit the sampled points, along with the RMS error.
fun FLine.fitLine3D(samples: Int = 20, sampler: FLineSampler? = null): Triple<Vec3, Vec3, Float> Fit a line to points on the curve using power iteration in 3D. Uses power iteration on the covariance matrix to find the principal eigenvector, which gives the best-fit line direction. Returns the line as a point (centroid) and direction vector, along with the RMS error.
fun FLine.fitPolynomial(degree: Int, samples: Int = 50, sampler: FLineSampler? = null): Pair<FloatArray, Float> Fit a polynomial of given degree to the curve in 2D. Uses Smile's OLS (Ordinary Least Squares) for robust fitting. Fits y = a0 + a1x + a2x^2 + ... + an*x^n
fun FLine.fitEllipse(samples: Int = 30, sampler: FLineSampler? = null): EllipseFit
fun FLine.fitBezierCubic(samples: Int = 20, sampler: FLineSampler? = null): Pair<Array<Vec3>, Float> Fit a cubic Bezier curve to the sampled points. Uses least squares to find control points that best approximate the curve. Can fit any number of points >= 2.
fun FLine.fitPiecewiseLinear(maxSegments: Int = 10, curvatureThreshold: Float = 0.05f, samples: Int = 100, sampler: FLineSampler? = null): List<FLine> Fit piecewise linear segments to the curve adaptively based on curvature. High curvature areas get more segments. Returns a list of line segments.
fun pointsToFLine(points: List<Vec3>, strategy: String = "adaptive"): FLine Create an FLine from a list of points with automatic curve fitting.
fun pointsToFLine(points: List<Vec2>, strategy: String = "adaptive"): FLine Create an FLine from a list of 2D points. Convenience function that converts Vec2 to Vec3 with z=0.
fun FLine.bySubdividing(levels: Int = 1): FLine Subdivide a curve using De Casteljau's algorithm. This creates a smoother approximation with more control points.
fun FLine.byArcLengthParameterizing(spacing: Float): FLine Arc-length parameterization: resample the curve with equal spacing along its length.
fun FLine.byDashing(dashLength: Float, gapLength: Float, resolution: Float = 2f): List<FLine> Create a dashed version of the curve.
fun FLine.byWaving(amplitude: Float, frequency: Float, samples: Int = 100): FLine Create a wave pattern along the curve.
fun FLine.byZigzagging(amplitude: Float, segments: Int = 20): FLine Create a zigzag pattern along the curve.
fun FLine.set2DPlane(origin: Vec3 = vec(0f, 0f, 0f), normal: Vec3 = vec(0f, 0f, 1f), xAxisHint: Vec3 = vec(1f, 0f, 0f)): FLine Set a local 2D plane used by numeric/Vec2 path builders. xAxisHint is projected onto the plane to define the X axis.
fun FLine.clear2DPlane(): FLine Clear any plane set by set2DPlane, returning to the default XY plane.
fun FLine.arcTo(p1: Vec3, p2: Vec3, radius: Float, segments: Int = 32): FLine Append a circular arc tangent to segments (current -> p1) and (p1 -> p2).
fun FLine.arcTo(p1: Vec2, p2: Vec2, radius: Number, segments: Int = 32): FLine 2D overload of arcTo using the active 2D plane.
fun FLine.arcTo(x1: Number, y1: Number, x2: Number, y2: Number, radius: Number, segments: Int = 32): FLine Numeric overload of arcTo using the active 2D plane.
fun FLine.circleThroughTo(through: Vec3, end: Vec3, segments: Int = 32): FLine Append a circular arc through a midpoint to an end point.
fun FLine.circleThroughTo(through: Vec2, end: Vec2, segments: Int = 32): FLine 2D overload of circleThroughTo using the active 2D plane.
fun FLine.circleThroughTo(throughX: Number, throughY: Number, endX: Number, endY: Number, segments: Int = 32): FLine Numeric overload of circleThroughTo using the active 2D plane.
fun FLine.polarCubicTo(c1Angle: Float, c1Length: Float, c2Angle: Float, c2Length: Float, endPoint: Vec3): FLine Append a cubic Bezier using polar handle angles/lengths in the active 2D plane.
fun FLine.polarCubicTo(c1Angle: Float, c1Length: Float, c2Angle: Float, c2Length: Float, endPoint: Vec2): FLine 2D overload of polarCubicTo using the active 2D plane.
fun FLine.polarCubicTo(c1Angle: Float, c1Length: Float, c2Angle: Float, c2Length: Float, x: Number, y: Number, z: Number = 0): FLine Numeric overload of polarCubicTo using the active 2D plane.
fun polylineFrom(points: List<Vec3>, closed: Boolean = false): FLine Build a polyline from a list of points.
fun polylineFrom(points: List<Vec2>, z: Number = 0, closed: Boolean = false): FLine 2D overload of polylineFrom, with an optional constant Z value.
fun FLine.byAppendingPolyline(points: Iterable<Vec3>, closed: Boolean = false): FLine Append a polyline to this line, honoring FLine.breakNext.
fun FLine.byAppendingPolyline(points: Iterable<Vec2>, z: Number = 0, closed: Boolean = false): FLine 2D overload of byAppendingPolyline, with an optional constant Z value.
fun catmullRomToBezier(points: List<Vec3>, closed: Boolean = false, tension: Float = 1f): FLine Catmull-Rom to cubic Bezier conversion (uniform, tension-scaled).
fun catmullRomToBezier(points: List<Vec2>, z: Number = 0, closed: Boolean = false, tension: Float = 1f): FLine 2D overload of catmullRomToBezier, with an optional constant Z value.
fun cubicSplineFrom(points: List<Vec3>, closed: Boolean = false, tension: Float = 1f): FLine Cubic spline builder (Catmull-Rom based).
fun cubicSplineFrom(points: List<Vec2>, z: Number = 0, closed: Boolean = false, tension: Float = 1f): FLine 2D overload of cubicSplineFrom, with an optional constant Z value.
fun FLine.smoothTo(endPoint: Vec3, smoothness: Float = 1f): FLine Append a smooth segment by enforcing a shared tangent at the join. If called immediately after a MoveTo, this falls back to lineTo. smoothness scales the (next - prev) / 6 handle length (1f = default).
fun FLine.smoothTo(x: Number, y: Number, z: Number = 0, smoothness: Float = 1f): FLine Numeric overload of smoothTo.
fun FLine.smoothTo(v: Vec2, smoothness: Float = 1f): FLine 2D overload of smoothTo.
fun FLine.tcbTo(endPoint: Vec3, tension: Float = 0f, continuity: Float = 0f, bias: Float = 0f): FLine Append a Kochanek-Bartels (TCB) spline segment incrementally. Tension/continuity/bias apply to the end point being added.
fun FLine.tcbTo(x: Number, y: Number, z: Number = 0, tension: Float = 0f, continuity: Float = 0f, bias: Float = 0f): FLine Numeric overload of tcbTo using the active 2D plane.
fun FLine.tcbTo(v: Vec2, tension: Float = 0f, continuity: Float = 0f, bias: Float = 0f): FLine 2D overload of tcbTo using the active 2D plane.
fun FLine.biarcTo(endPoint: Vec3, startTangent: Vec3? = null, endTangent: Vec3? = null, join: Float = 0.5f, joinTangentMix: Float = 0.5f, bulge: Float = 0f, segments: Int = 32, planeNormal: Vec3? = null, joinTangent: Vec3? = null): FLine Append a biarc (two circular arcs) with parametric control. join controls the split point along the chord; joinTangentMix blends start/end tangents at the join. bulge offsets the join point sideways (scaled by chord length).
fun FLine.biarcTo(endPoint: Vec2, startHeading: Float? = null, endHeading: Float? = null, join: Float = 0.5f, joinTangentMix: Float = 0.5f, bulge: Float = 0f, segments: Int = 32): FLine 2D overload of biarcTo using heading angles in radians.
fun FLine.biarcTo(x: Number, y: Number, startHeading: Float? = null, endHeading: Float? = null, join: Float = 0.5f, joinTangentMix: Float = 0.5f, bulge: Float = 0f, segments: Int = 32): FLine Numeric overload of biarcTo using the active 2D plane.
fun biarcBetween(start: Vec3, end: Vec3, startTangent: Vec3, endTangent: Vec3, join: Float = 0.5f, joinTangentMix: Float = 0.5f, bulge: Float = 0f, segments: Int = 32, planeNormal: Vec3? = null, joinTangent: Vec3? = null): FLine Build a standalone biarc between two points with specified tangents.
fun FLine.byAppendingArc3D(center: Vec3, radius: Float, startAngle: Float, endAngle: Float, normal: Vec3 = vec(0f, 0f, 1f), segments: Int = 32): FLine Append a circular arc to this FLine in 3D.
fun arcThrough3Points(p1: Vec3, p2: Vec3, p3: Vec3, segments: Int = 32): FLine Create a circular arc through three points in 3D. Returns an FLine containing the arc.
fun FLine.byAppendingQuadratic(controlPoint: Vec3, endPoint: Vec3): FLine Append a quadratic Bezier curve to this FLine. Converts to cubic Bezier internally.
fun FLine.byAppendingArcByTangent(endPoint: Vec3, startTangent: Vec3, endTangent: Vec3, segments: Int = 32): FLine Append a circular arc to this FLine by specifying tangent at start and end. Uses the arc that best fits the tangent constraints.
fun FLine.byGaussianNoising(amplitude: Float, seed: Long = System.currentTimeMillis(), samples: Int = 100, sampler: FLineSampler? = null): FLine Apply Gaussian noise to this curve.
fun FLine.byOrientedGaussianNoising(normalAmplitude: Float, tangentAmplitude: Float = 0f, seed: Long = System.currentTimeMillis(), samples: Int = 100): FLine Apply direction-sensitive (oriented) Gaussian noise to this curve. Noise is applied primarily along the normal direction.
fun FLine.byPerlinNoising(amplitude: Float, frequency: Float = 1f, octaves: Int = 4, seed: Long = System.currentTimeMillis(), samples: Int = 100): FLine Apply Perlin noise to this curve.
fun FLine.byAddingWith(other: FLine, samples: Int = 100, sampler: FLineSampler? = null, otherSampler: FLineSampler? = sampler): FLine Add two curves point-wise. Samples both curves and adds corresponding points.
fun FLine.bySubtractingWith(other: FLine, samples: Int = 100, sampler: FLineSampler? = null, otherSampler: FLineSampler? = sampler): FLine Subtract one curve from another point-wise.
fun FLine.byProjectingOnto(other: FLine, samples: Int = 100, sampler: FLineSampler? = null): FLine Project one curve onto another. For each point on line1, finds the closest point on line2.
fun FLine.byInverting(center: Vec2 = vec(0f, 0f), radius: Float = 100f, samples: Int = 100, sampler: FLineSampler? = null): FLine Invert a curve through a point (geometric inversion). Each point is inverted through a circle centered at 'center' with given 'radius'.
fun FLine.byMultiplyingWith(other: FLine, samples: Int = 100, sampler: FLineSampler? = null, otherSampler: FLineSampler? = sampler): FLine Multiply two curves point-wise (component-wise multiplication of coordinates).
fun FLine.byOffsettingParameterized(distanceFunc: ParameterFunction, samples: Int = 100): FLine Apply a parameterized offset to a curve. The offset distance varies along the curve according to the parameter function.
fun FLine.byNoisingParameterized(amplitudeFunc: ParameterFunction, seed: Long = System.currentTimeMillis(), samples: Int = 100): FLine Apply parameterized noise to a curve. The noise amplitude varies according to the parameter function.
fun FLine.byScalingParameterized(scaleFunc: ParameterFunction, center: Vec2 = vec(0f, 0f), samples: Int = 100, sampler: FLineSampler? = null): FLine Apply parameterized scaling to a curve around a center point.
fun parameterFromLambda(func: (t: Float, pos: Vec3) -> Float): ParameterFunction Create a parameter function from a simple lambda.
fun bsplineFrom(controlPoints: List<Vec3>, degree: Int = 3, samples: Int = 200, closed: Boolean = false): FLine Evaluate a uniform open B-spline of arbitrary degree using Cox-de Boor.
fun FLine.byBSplineSmoothed(degree: Int = 4, controlSampleCount: Int = 24, samples: Int = 200, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, closed: Boolean = false, controlSampler: FLineSampler? = null, outputSampler: FLineSampler? = null): FLine Reinterpret an existing curve through a higher-order B-spline fit.
fun eulerSpiralBetween3D(start: Vec3, startDirection: Vec3, end: Vec3, endDirection: Vec3, samples: Int = 200, iterations: Int = 6, lengthScale: Float = 1f, headingScale: Float = 1f, turn: Float = 0f): FLine Build an Euler spiral (clothoid) from start to end that leaves along startDirection and arrives along endDirection.
fun FLine.byEulerSpiralTo(endPoint: Vec3, startDirection: Vec3? = null, endDirection: Vec3? = null, samples: Int = 200, iterations: Int = 6, lengthScale: Float = 1f, headingScale: Float = 1f, turn: Float = 0f): FLine Return a new line with an Euler spiral appended from the current end to endPoint.
fun FLine.eulerSpiralTo(endPoint: Vec3, startDirection: Vec3? = null, endDirection: Vec3? = null, samples: Int = 200, iterations: Int = 6, lengthScale: Float = 1f, headingScale: Float = 1f, turn: Float = 0f): FLine Append an Euler spiral from the current end to endPoint.
fun FLine.eulerSpiralTo(x: Number, y: Number, z: Number = 0, startDirection: Vec3? = null, endDirection: Vec3? = null, samples: Int = 200, iterations: Int = 6, lengthScale: Float = 1f, headingScale: Float = 1f, turn: Float = 0f): FLine Numeric overload of eulerSpiralTo using the active 2D plane.
fun FLine.eulerSpiralTo(v: Vec2, startDirection: Vec3? = null, endDirection: Vec3? = null, samples: Int = 200, iterations: Int = 6, lengthScale: Float = 1f, headingScale: Float = 1f, turn: Float = 0f): FLine 2D overload of eulerSpiralTo using the active 2D plane.
fun FLine.byCurvatureFlow(step: Float = 0.25f, iterations: Int = 5, samples: Int = 200, anchorEnds: Boolean = true, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, sampler: FLineSampler? = null): FLine Iterate mean-curvature flow on a sampled polyline.
fun FLine.byGaussianProcessInterpolating(trainingSamples: Int = 48, samples: Int = 200, lengthScale: Float = 0.15f, variance: Float = 1f, noise: Float = 1e-3f, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, drawSamples: Boolean = false, seed: Long? = null, trainingSampler: FLineSampler? = null, outputSampler: FLineSampler? = null): FLine RBF-kernel Gaussian Process interpolation along arc length, applied independently to XYZ.
fun FLine.byEmaFiltering(alpha: Float = 0.18f, samples: Int = 200, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, sampler: FLineSampler? = null): FLine Simple exponential moving average over sampled points.
fun FLine.byOneEuroFiltering(minCutoffHz: Float = 1.5f, beta: Float = 0.03f, dCutoffHz: Float = 1f, fpsHint: Float = 60f, samples: Int = 200, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, sampler: FLineSampler? = null): FLine One-Euro filter on 3D positions with a fixed dt inferred from an FPS hint.
fun FLine.byAlphaBetaFiltering(alpha: Float = 0.25f, beta: Float = 0.08f, fpsHint: Float = 90f, samples: Int = 200, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, sampler: FLineSampler? = null): FLine Lightweight constant-velocity alpha-beta filter applied to a curve.
fun FLine.byDriverSmoothing(config: FLineDriverSmoothingConfig = FLineDriverSmoothingConfig(), samples: Int = 200, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, sampler: FLineSampler? = null): FLine Driver-style pursuit smoother over a sampled polyline.
fun FLine.byOneDollarFiltering(targetSamples: Int = 64, targetSize: Float = 200f, rotateToZero: Boolean = true, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, sampler: FLineSampler? = null): FLine One-Dollar style normalization/resampling (rotation + scale + re-sample). Useful as a "filter" to regularize noisy gesture paths.
fun catmullRomCentripetal(points: List<Vec2>, alpha: Double = 0.5): FLine A smooth curve through every point of points: a centripetal Catmull-Rom spline, as cubic segments.
fun FLine.byWiggling(amplitude: Double = 10.0, frequency: Double = 0.05, phase: Double = 0.0): FLine Every node of this line (and every cubic's control points) pushed about by 2D Perlin noise (ClassicPerlin2D): amplitude in the line's own units, frequency in noise cycles per unit, and phase sliding the noise field, so phase = t makes the wiggle move.
fun FLine.byAtomizing(): FLine This line broken into separate segments: every drawn piece of it becomes its own moveTo/lineTo subpath, between the same end points (a cubic is replaced by its chord). Each segment carries the properties of the node that ended it.
fun FLine.byTransformingTo(start: Vec2, end: Vec2): FLine This line rotated, scaled and moved as one rigid shape so that its first node lands on start and its last on end. A line whose two ends coincide is returned unchanged, since no rotation would be defined.

Top-level values

Declaration Summary
val start
val end
val midpoint
val center

Vec2Anchor

typealias Vec2Anchor = (List<Vec2>) -> Vec2

Vec3Anchor

typealias Vec3Anchor = (List<Vec3>) -> Vec3

ListAnchor

enum class ListAnchor

Constants: Start, End, Midpoint, Center.

FractionAlongAnchor

data class FractionAlongAnchor(val t: Number)

Anchors2

object Anchors2

Declaration Summary
val start: Vec2Anchor
val end: Vec2Anchor
val midpoint: Vec2Anchor
val center: Vec2Anchor
fun fractionAlong(t: Number): Vec2Anchor

Anchors3

object Anchors3

Declaration Summary
val start: Vec3Anchor
val end: Vec3Anchor
val midpoint: Vec3Anchor
val center: Vec3Anchor
fun fractionAlong(t: Number): Vec3Anchor

EllipseFit

data class EllipseFit(val center: Vec2, val semiMajorAxis: Float, val semiMinorAxis: Float, val rotationAngle: Float, val error: Float)

Fit an ellipse to points on the curve using moment-based approximation. Note: This uses a simplified moment-based method rather than full algebraic distance minimization. It provides good approximations for well-behaved elliptical data.

Declaration Summary
val center: Vec2
val semiMajorAxis: Float
val semiMinorAxis: Float
val rotationAngle: Float
val error: Float

ParameterFunction

interface ParameterFunction

Interface for parameter functions that can vary along a curve. This allows any numeric parameter to be a function of position along the curve.

Declaration Summary
fun evaluate(t: Float, position: Vec3): Float Evaluate the parameter at normalized position t and curve position.

ConstantParameter

class ConstantParameter(val value: Float) : ParameterFunction

Constant parameter (doesn't vary).

Declaration Summary
override fun evaluate(t: Float, position: Vec3)
override fun toString(): String

LinearParameter

class LinearParameter(val start: Float, val end: Float) : ParameterFunction

Linear interpolation from start to end value.

Declaration Summary
override fun evaluate(t: Float, position: Vec3)
override fun toString(): String

CurveParameter

class CurveParameter(val curve: FLine, val scale: Float = 1f, val offset: Float = 0f) : ParameterFunction

Parameter driven by another curve's Y coordinate.

Declaration Summary
override fun evaluate(t: Float, position: Vec3): Float
override fun toString(): String

DistanceParameter

class DistanceParameter(val point: Vec3, val minValue: Float, val maxValue: Float, val maxDistance: Float) : ParameterFunction

Parameter driven by distance from a point.

Declaration Summary
val point: Vec3
val minValue: Float
val maxValue: Float
val maxDistance: Float
override fun evaluate(t: Float, position: Vec3): Float
override fun toString(): String

CurvatureParameter

class CurvatureParameter(val line: FLine, val minValue: Float, val maxValue: Float, val maxCurvature: Float = 1f) : ParameterFunction

Parameter driven by curvature.

Declaration Summary
val line: FLine
val minValue: Float
val maxValue: Float
val maxCurvature: Float
override fun evaluate(t: Float, position: Vec3): Float
override fun toString(): String

SineParameter

class SineParameter(val amplitude: Float, val frequency: Float, val phase: Float = 0f, val offset: Float = 0f) : ParameterFunction

Sine wave parameter.

Declaration Summary
val amplitude: Float
val frequency: Float
val phase: Float
val offset: Float
override fun evaluate(t: Float, position: Vec3)
override fun toString(): String

FLineDriverTargetUpdateMode

enum class FLineDriverTargetUpdateMode

How a driver-style smoother is allowed to retarget the sampled source line.

Constants: FORWARD_ONLY, WINDOWED_NEAREST, STICKY_WINDOWED_NEAREST.

FLineDriverSmoothingConfig

data class FLineDriverSmoothingConfig(val maxAngularVelocity: Float = 0.3f, val maxAngularAcceleration: Float = 0.06f, val lookaheadSamples: Int = 4, val forwardSearchSamples: Int = 18, val backwardSearchSamples: Int = 6, val backwardRetargetHysteresis: Float = 0.15f, val linearStepScale: Float = 1f, val targetUpdateMode: FLineDriverTargetUpdateMode = FLineDriverTargetUpdateMode.STICKY_WINDOWED_NEAREST)

Configuration for FLine.byDriverSmoothing.

Declaration Summary
val maxAngularVelocity: Float
val maxAngularAcceleration: Float
val lookaheadSamples: Int
val forwardSearchSamples: Int
val backwardSearchSamples: Int
val backwardRetargetHysteresis: Float
val linearStepScale: Float
val targetUpdateMode: FLineDriverTargetUpdateMode

ClassicPerlin2D

object ClassicPerlin2D

Classic 2D Perlin noise with a fixed permutation table, in about -1..1, zero at integer lattice points.

Declaration Summary
fun noise(x: Double, y: Double): Double The noise at (x, y).

FLineGeometrySmile.kt

Resamples an FLine through a fitted curve: bySmileSplineInterpolating (Smile cubic spline), bySmileGaussianProcessInterpolating (Gaussian process) and byAkimaInterpolating.

Each one samples the line, fits X, Y and Z separately against normalised arc length, and returns a new line of samples points, mixed with the original by dryWet.

src/jvmMain/kotlin/afield/geometry/path/FLineGeometrySmile.kt — read the source

Top-level functions

Declaration Summary
fun FLine.bySmileSplineInterpolating(trainingSamples: Int = 48, samples: Int = 200, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, trainingSampler: FLineSampler? = null, outputSampler: FLineSampler? = null): FLine Smile cubic-spline interpolation along arc-length, applied independently to XYZ.
fun FLine.bySmileGaussianProcessInterpolating(trainingSamples: Int = 48, samples: Int = 200, lengthScale: Float = 0.15f, noise: Float = 1e-3f, normalize: Boolean = true, tol: Float = 1e-4f, maxIter: Int = 500, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, trainingSampler: FLineSampler? = null, outputSampler: FLineSampler? = null): FLine Smile Gaussian-process interpolation along arc-length, applied independently to XYZ.
fun FLine.byAkimaInterpolating(trainingSamples: Int = 48, samples: Int = 200, dryWet: Float = 1f, wetModulator: ParameterFunction? = null, trainingSampler: FLineSampler? = null, outputSampler: FLineSampler? = null): FLine Custom Akima-style interpolation along arc-length, applied independently to XYZ.

FLineHyperbezier.kt

Fits ordinary cubics to Hyperbezier curves, so that family is usable everywhere an FLine already accepts a cubic.

A hyperbezier is specified exactly the way a cubic Bezier is: a start, two control points, an end. It is a different curve through that polygon, one whose curvature varies smoothly, and it is an exact circular arc or Euler spiral when the arms are "circle-like" (see circleArm). Push the arms out past that and the curve tightens into a corner instead of bulging. What it will not do is the cubic's double inflection: a hyperbezier has at most one.

Nothing downstream of an FLine needs to know any of that. fitHyperbezierCubics hands back a handful of ordinary cubics, G1-continuous by construction because every join is pinned to the hyperbezier's own tangent there. To draw one, use FLine.hyperbezierTo in place of FLine.cubicTo, or hyperbezierBetween for a whole line from one control polygon and hyperbezierThrough to run a curve through a list of points.

The curve itself, and the control-polygon-to-curve map, are in Hyperbezier.

src/jvmMain/kotlin/afield/geometry/path/FLineHyperbezier.kt — read the source

Used in: FLine examples (Field graphics)

Top-level functions

Declaration Summary
fun fitHyperbezierCubics(p0: Vec3, control1: Vec3, control2: Vec3, p3: Vec3, tolerance: Number = 1e-4, maxSegments: Int = 16, cornerBias: Number = 1.0, armResponse: Number = 2.0): HyperbezierFit Fit cubics to the hyperbezier through the cubic-shaped control polygon p0, control1, control2, p3.
fun hyperbezierBetween(p0: Vec3, control1: Vec3, control2: Vec3, p3: Vec3, tolerance: Number = 1e-4, maxSegments: Int = 16, cornerBias: Number = 1.0, armResponse: Number = 2.0): FLine A new line tracing the hyperbezier through the control polygon p0, control1, control2, p3, as cubics.
fun FLine.hyperbezierTo(control1: Vec3, control2: Vec3, endPoint: Vec3, tolerance: Number = 1e-4, maxSegments: Int = 16, cornerBias: Number = 1.0, armResponse: Number = 2.0): FLine Append a hyperbezier from the current end to endPoint, as cubics — the drop-in swap for FLine.cubicTo.
fun FLine.byHyperbezierTo(control1: Vec3, control2: Vec3, endPoint: Vec3, tolerance: Number = 1e-4, maxSegments: Int = 16, cornerBias: Number = 1.0, armResponse: Number = 2.0): FLine A copy of this line with a hyperbezier appended from its end to endPoint, as cubics.
fun FLine.hyperbezierTo(x0: Number, y0: Number, x1: Number, y1: Number, x: Number, y: Number, tolerance: Number = 1e-4, maxSegments: Int = 16): FLine Numeric overload of hyperbezierTo using the active 2D plane.
fun FLine.hyperbezierTo(control1: Vec2, control2: Vec2, endPoint: Vec2, tolerance: Number = 1e-4, maxSegments: Int = 16): FLine 2D overload of hyperbezierTo using the active 2D plane.
fun hyperbezierThrough(points: List<Vec3>, tension: Number = 1.0, closed: Boolean = false, tolerance: Number = 1e-4, maxSegments: Int = 16, cornerBias: Number = 1.0, armResponse: Number = 2.0): FLine A smooth hyperbezier spline through points, as cubics.

HyperbezierCubic

data class HyperbezierCubic(val control1: Vec3, val control2: Vec3, val end: Vec3)

One cubic of a fit — the two control points and the endpoint, as FLine.cubicTo wants them.

Declaration Summary
val control1: Vec3 First control point, off the start of this piece.
val control2: Vec3 Second control point, off the end of this piece.
val end: Vec3 Endpoint of this piece, and the start of the next.

HyperbezierFit

class HyperbezierFit(val start: Vec3, val cubics: List<HyperbezierCubic>, val maxError: Float, val tolerance: Float, val curve: Hyperbezier?)

The cubics fitHyperbezierCubics fitted to a hyperbezier, plus how well they did.

Declaration Summary
val start: Vec3 Where the curve starts — the p0 handed to the fit.
val cubics: List<HyperbezierCubic> The fitted cubics, in order.
val maxError: Float The largest distance, in world units, from the true hyperbezier to the fitted cubics. Zero when curve is null, since there is then no hyperbezier to have missed.
val tolerance: Float The tolerance the fit was asked for, in world units.
val curve: Hyperbezier? The curve that was fitted, or null if the solver stalled or the polygon was degenerate — in which case cubics is the single cubic that was passed in, unchanged.
val solved: Boolean False when the fit fell back to the input cubic; see curve.
val toleranceMet: Boolean True when the fit came in under the tolerance it was asked for.
val segmentCount: Int How many cubics the fit used.
fun toFLine(): FLine A new line containing just this fit, opening with a moveTo to start.
fun appendTo(line: FLine): FLine Append this fit to line as cubics. If line is empty it is opened with a moveTo to start; otherwise the caller is responsible for line already ending at start.
override fun toString(): String

FLineInstructionTrajectories.kt

Traces how the points of a changing drawing move from frame to frame, as one trajectory line per point.

FLineSideways.update takes each frame's line(s) and matches their anchor points to the previous frame's; FLineSideways.trajectories and FLineSideways.trajectoryMap return the tracks drawn so far.

src/jvmMain/kotlin/afield/geometry/path/FLineInstructionTrajectories.kt — read the source

FLineSideways

class FLineSideways(val maxMatchDistance: Float = Float.POSITIVE_INFINITY, val stableTopologyDistanceThreshold: Float = if (maxMatchDistance.isFinite()) maxMatchDistance else 0f, val maxMissedFrames: Int = 8, val breakOnGap: Boolean = true, val includeCloseInstructions: Boolean = false, val instructionIndexWeight: Double = 0.05, val lineKeySelector: (lineIndex: Int, line: FLine) -> Any = { lineIndex, _ -> lineIndex })

Incrementally tracks drawing-instruction anchor points across frames and emits one trajectory FLine per tracked point.

Declaration Summary
val maxMatchDistance: Float
val stableTopologyDistanceThreshold: Float
val maxMissedFrames: Int
val breakOnGap: Boolean
val includeCloseInstructions: Boolean
val instructionIndexWeight: Double
val lineKeySelector: (lineIndex: Int, line: FLine) -> Any
val activeTrackCount: Int Number of currently active tracks.
val totalTrackCount: Int Number of total tracks (active + archived).
fun reset() Clears all state and trajectories.
fun update(line: FLine): List<FLine> Update with a single line frame.
fun update(lines: Iterable<FLine>): List<FLine> Update with a frame of lines.
fun update(lines: Map<*, FLine>): List<FLine> Update with explicitly keyed lines (handy when source line order can change).
fun trajectories(includeArchived: Boolean = true): List<FLine> Return trajectories ordered by creation id.
fun trajectoryMap(includeArchived: Boolean = true): FLineMap Return trajectories as an FLineMap (track_<id> keys).

FLineInterpolation.kt

Morphing one FLine into another: align, match and blend two drawings so any in-between shape can be drawn.

interpolatorTo builds a reusable FLineInterpolator from an FLineInterpolationConfig (alignment, matching and motion settings), and FLineInterpolator.lineAt returns the line at a given t. byInterpolatingTo is the one-shot form for a single evaluation.

src/jvmMain/kotlin/afield/geometry/path/FLineInterpolation.kt — read the source

Used in: FLine examples (Field graphics)

Top-level functions

Declaration Summary
fun FLine.interpolatorTo(other: FLine, config: FLineInterpolationConfig = FLineInterpolationConfig()): FLineInterpolator Create a reusable interpolator to other.
fun FLine.byInterpolatingTo(other: FLine, t: Float, config: FLineInterpolationConfig = FLineInterpolationConfig(), motion: FLineInterpolationMotion = config.motion): FLine One-shot convenience wrapper around FLineInterpolator.

FLineInterpolationScaleMode

enum class FLineInterpolationScaleMode

Uniform scale normalization used during interpolation setup.

Constants: NONE, RMS_RADIUS, BOUNDS_DIAGONAL.

FLineInterpolationRotationMode

enum class FLineInterpolationRotationMode

Pre-alignment rotation used during interpolation setup.

Constants: NONE, FIT_LINE_2D, FIT_LINE_3D.

FLineInterpolationSpinMode

enum class FLineInterpolationSpinMode

Evaluation-time envelope for spin-style motion modifiers.

Constants: LINEAR, MIDPOINT_RETURN.

Declaration Summary
fun envelope(t: Float): Float Reach full value at t = 0.5, then return to zero so the final keyframe is hit exactly.

FLineInterpolationAlignment

data class FLineInterpolationAlignment(val translateToCenter: Boolean = true, val scaleMode: FLineInterpolationScaleMode = FLineInterpolationScaleMode.RMS_RADIUS, val rotationMode: FLineInterpolationRotationMode = FLineInterpolationRotationMode.NONE)

Geometric normalization before matching.

Declaration Summary
val translateToCenter: Boolean
val scaleMode: FLineInterpolationScaleMode
val rotationMode: FLineInterpolationRotationMode

FLineInterpolationMatching

data class FLineInterpolationMatching(val denseSampleCount: Int = 256, val extraAnchorSamplesPerLine: Int = 0, val minimumBasisCount: Int = 64, val maximumBasisCount: Int = 1024, val basisDensityMultiplier: Int = 4, val unmatchedCost: Double = 4.0, val assignmentOrderPenalty: Double = 0.15, val projectionOrderPenalty: Double = 0.25, val assignmentConstraintWeight: Float = 2f, val projectionConstraintWeight: Float = 1f, val boundaryConstraintWeight: Float = 8f, val allowReverseOrder: Boolean = true, val allowClosedOrderRotation: Boolean = true, val closedOrderRotationSteps: Int? = null)

Setup-time matching and resampling controls.

Declaration Summary
val denseSampleCount: Int
val extraAnchorSamplesPerLine: Int
val minimumBasisCount: Int
val maximumBasisCount: Int
val basisDensityMultiplier: Int
val unmatchedCost: Double
val assignmentOrderPenalty: Double
val projectionOrderPenalty: Double
val assignmentConstraintWeight: Float
val projectionConstraintWeight: Float
val boundaryConstraintWeight: Float
val allowReverseOrder: Boolean
val allowClosedOrderRotation: Boolean
val closedOrderRotationSteps: Int?

FLineInterpolationMotion

data class FLineInterpolationMotion(val spatialSpinAngle: Float = 0f, val spatialSpinAxis: Vec3 = vec(0f, 0f, 1f), val spatialSpinMode: FLineInterpolationSpinMode = FLineInterpolationSpinMode.LINEAR, val orderSpin: Float = 0f, val orderSpinMode: FLineInterpolationSpinMode = FLineInterpolationSpinMode.LINEAR, val wrapOrderSpin: Boolean = false)

Fast evaluation-time motion controls applied after the cached basis blend.

Declaration Summary
val spatialSpinAngle: Float
val spatialSpinAxis: Vec3
val spatialSpinMode: FLineInterpolationSpinMode
val orderSpin: Float
val orderSpinMode: FLineInterpolationSpinMode
val wrapOrderSpin: Boolean

FLineInterpolationConfig

data class FLineInterpolationConfig(val alignment: FLineInterpolationAlignment = FLineInterpolationAlignment(), val matching: FLineInterpolationMatching = FLineInterpolationMatching(), val motion: FLineInterpolationMotion = FLineInterpolationMotion(), val samplerA: FLineSampler = FLineSamplers.curvatureAdaptive(gain = 3f, oversample = 4, minBaseSamples = 48), val samplerB: FLineSampler = samplerA, val closed: Boolean? = null)

Configuration for FLineInterpolator.

Declaration Summary
val alignment: FLineInterpolationAlignment
val matching: FLineInterpolationMatching
val motion: FLineInterpolationMotion
val samplerA: FLineSampler
val samplerB: FLineSampler
val closed: Boolean?

FLineInterpolationOrderTransform

data class FLineInterpolationOrderTransform(val reverse: Boolean = false, val shift: Float = 0f)

Reparameterization applied to the second line while solving the correspondence.

Declaration Summary
val reverse: Boolean
val shift: Float
fun forward(rawU: Float, closed: Boolean): Float
fun inverse(orderedU: Float, closed: Boolean): Float

FLineInterpolationDiagnostics

data class FLineInterpolationDiagnostics(val orderTransform: FLineInterpolationOrderTransform, val assignmentCost: Double, val warpKnots: List<Vec2>, val basisCount: Int, val denseSampleCountA: Int, val denseSampleCountB: Int, val outputClosed: Boolean)

Introspection for the chosen interpolation setup.

Declaration Summary
val orderTransform: FLineInterpolationOrderTransform
val assignmentCost: Double
val warpKnots: List<Vec2>
val basisCount: Int
val denseSampleCountA: Int
val denseSampleCountB: Int
val outputClosed: Boolean

FLineInterpolator

class FLineInterpolator(val from: FLine, val to: FLine, val config: FLineInterpolationConfig = FLineInterpolationConfig())

Slow-to-configure, fast-to-evaluate single-contour FLine morphing primitive.

Declaration Summary
val from: FLine
val to: FLine
val config: FLineInterpolationConfig
val diagnostics: FLineInterpolationDiagnostics
val basisCount: Int Number of cached basis points used by this interpolator.
fun sourceBasis(): Pair<FLine, FLine> Return the cached source basis lines used for evaluation.
fun pointsAt(t: Float, motion: FLineInterpolationMotion = config.motion): List<Vec3> Generate the interpolated point sequence at t.
fun writeInto(target: FLine, t: Float, motion: FLineInterpolationMotion = config.motion): FLine Write the interpolated geometry into target, clearing it first.
fun lineAt(t: Float, motion: FLineInterpolationMotion = config.motion): FLine Generate a new FLine at t.

FLineMeasure.kt

Measuring, locating, trimming and crossing an FLine.

There is one measurer: FLine.cursor. A cursor walks a line by distance (setD) or by node parameter (setT), and everything it reports about the point under it — position, unit tangent, Frenet frame, curvature — comes from the one segment evaluator in this file: exact cubic first and second derivatives, straight runs degree-elevated to cubics, degenerate ends handled. location() hands the lot back as an FLineLocation.

The FLine-level functions here are front doors onto a cursor for the common one-shot questions: length, locationAt, locationAtFraction, nearestLocation, trim, splitAt. Use a cursor yourself when you walk a line step by step.

Two parameterisations run through all of it:

  • distance (d), in drawing units along the line, from 0 to length. Use it when you want things evenly spaced.
  • t, the node parameter: t = i + u is fraction u of the way (by curve parameter, not by distance) along the segment that ends at node i + 1.

Every FLineLocation carries both.

Everything whose answer only exists in the xy plane says so in its name: FLineLocation.normal2D, FLineLocation.signedCurvature2D, tightBounds2D, area2D, isClockwise2D, windingNumber2D, contains2D, crossings2D, selfCrossings2D. Everything else is 3D.

src/jvmMain/kotlin/afield/geometry/path/FLineMeasure.kt — read the source

Used in: FLine examples (Field graphics)

Top-level functions

Declaration Summary
fun FLine.length(): Float The total drawn length of this line, in drawing units — MoveTo jumps do not count, Close segments do. cursor().lengthD(); Paper.js path.length, Skia SkPathMeasure.getLength.
fun FLine.locationAt(distance: Float): FLineLocation? The location distance units along this line (clamped to 0..length()): cursor().setD(distance).location(). Paper.js getLocationAt and its getTangentAt / getNormalAt / getCurvatureAt family; Skia SkPathMeasure.getPosTan. null for a line with nothing drawn in it.
fun FLine.locationAtFraction(fraction: Float): FLineLocation? locationAt by fraction of the length: 0 is the start, 1 the end, 0.5 half way by distance.
fun FLine.nearestLocation(point: Vec3): FLineLocation? The location on this line nearest to point, by 3D distance: cursor(point).location(). Paper.js getNearestLocation. null for a line with nothing drawn in it.
fun FLine.trim(from: Float, to: Float): FLine The piece of this line between from and to units along it (both clamped to 0..length(); swapped if reversed). Cubics are cut exactly, so the result draws on top of the original. Skia SkPathMeasure.getSegment, Processing-style "draw the first 30% of a path" animations.
fun FLine.splitAt(distance: Float): Pair<FLine, FLine> This line cut in two at distance units along it: first runs from the start to the cut, second from the cut to the end. Both are always non-null (one may be a single moveTo), and first.length() + second.length() is the original length. Paper.js splitAt.
fun FLine.tightBounds2D(): Rect The axis-aligned 2D bounds of what this line actually draws — tight on cubics (it finds their extrema), unlike FLine.fast2DBounds, which encloses the control points. MoveTo points with nothing drawn from them still count, as in Paper.js bounds. A line with no nodes has an empty Rect(0, 0, 0, 0).
fun FLine.area2D(): Float The signed area this line encloses in the xy plane, exact on cubics. Each subpath is closed back to its start if it is not already, and subpaths add: a counter wound the other way subtracts. Positive when the contours run counter-clockwise with y pointing up — which is clockwise on screen, where y points down. Paper.js path.area.
fun FLine.isClockwise2D(): Boolean Whether this line winds clockwise on screen (y pointing down, as on Field's canvas and in SVG / Paper.js) — i.e. area2D is positive. A line with no area answers false.
fun FLine.crossings2D(other: FLine, tolerance: Float = 0.01f): List<FLineCrossing> Every point where this line crosses other in the xy plane, with the parameter on both lines — Paper.js getIntersections, Skia SkPathOps building block. Straight segments are intersected exactly; cubics by subdivision to tolerance drawing units. Hits closer than 4 * tolerance are one crossing. Sorted by FLineCrossing.t. Overlapping collinear runs report no isolated crossing.
fun FLine.selfCrossings2D(tolerance: Float = 0.01f): List<FLineCrossing> Every point where this line crosses itself, as pairs of parameters t < otherT — Paper.js getCrossings() on one path. Neighbouring segments meeting at their shared node, and a closed subpath meeting itself at its start, are joins, not crossings, and are not reported.
fun FLine.windingNumber2D(point: Vec2): Int The winding number of this line about point in the xy plane, following its curves (cubics are flattened finely), with every subpath treated as closed back to its moveTo; the jump between subpaths is not a segment. Its sign follows the travel direction; non-zero means inside under the non-zero rule. A glyph's counter, wound the other way, cancels to 0 inside the hole.
fun FLine.contains2D(point: Vec2, rule: FLineFillRule = FLineFillRule.NonZero): Boolean Whether point is inside this line in the xy plane under rule (non-zero by default), following its curves, with every subpath treated as closed. Use FLineFillRule.EvenOdd when a counter should be a hole regardless of its winding direction.

FLineLocation

data class FLineLocation(val point: Vec3, val tangent: Vec3, val normal: Vec3?, val binormal: Vec3?, val curvature: Float, val normal2D: Vec2, val signedCurvature2D: Float, val distance: Float, val t: Float, val segment: Int)

A point on an FLine with its local frame.

Declaration Summary
val point: Vec3 Where on the line.
val tangent: Vec3 Unit direction of travel. At a degenerate end — a cubic whose control point sits on its end point, where the derivative vanishes — it is the limiting direction. Zero only on a segment of zero length.
val normal: Vec3? The unit principal normal: perpendicular to tangent, towards the centre of curvature, in whatever plane the line is bending. null on straight runs, where there is no such direction — there is no sensible fallback in 3D, and in 2D you want normal2D anyway.
val binormal: Vec3? tangent × normal, the axis the line is turning about; null where normal is.
val curvature: Float Unsigned 3D curvature \|r' × r''\| / \|r'\|³, i.e. 1 / radius; 0 on straight runs.
val normal2D: Vec2 tangent's xy part turned a quarter turn, (tangent.y, -tangent.x), unit length. On Field's canvas (y down) that is to the left of the direction of travel; with y up, to the right. It is defined on straight runs too, and is the zero vector only where the line runs straight along z. This is the normal to offset a 2D drawing along.
val signedCurvature2D: Float Curvature of the line's xy projection, positive when it bends towards normal2D; 0 on straight runs.
val distance: Float Distance along the line from its start, in drawing units.
val t: Float The node parameter of this point (see the file overview).
val segment: Int Index of the node that ends the segment this point is on.

FLineCrossing

data class FLineCrossing(val point: Vec3, val t: Float, val otherT: Float)

One place where two lines (or one line with itself) cross in the xy plane.

Declaration Summary
val point: Vec3 The crossing point.
val t: Float The node parameter on the line the method was called on.
val otherT: Float The node parameter on the other line (on the same line, for selfCrossings2D).

FLineFillRule

enum class FLineFillRule

How contains2D decides "inside" when contours overlap or nest — the SVG / canvas fill-rule.

Constants: NonZero, EvenOdd.

FLineSampling.kt

Choosing points along an FLine: pluggable samplers that return parameter and position pairs.

FLineSamplers holds the built-in FLineSamplers (uniform, by distance, random, curvature adaptive, Douglas-Peucker, patterned) and FLineSamplers.compose / then chain them. sampleSeries returns FLineSamples and samplePoints returns just the points.

src/jvmMain/kotlin/afield/geometry/path/FLineSampling.kt — read the source

Top-level functions

Declaration Summary
infix fun FLineSampler.then(next: FLineSampler): FLineSampler Compose two samplers in sequence.
fun FLine.sampleSeries(samples: Int, sampler: FLineSampler? = null, seed: Long? = null, pointSetAware: Boolean = true): List<FLineSample> Sample an FLine into FLineSample values.
fun FLine.samplePoints(samples: Int, sampler: FLineSampler? = null, seed: Long? = null, pointSetAware: Boolean = true): List<Vec3> Sample an FLine into plain points.

FLineSample

data class FLineSample(val t: Float, val point: Vec3)

A sampled point on an FLine, with normalized parameter t in [0, 1].

Declaration Summary
override fun toString(): String

FLineSampleContext

data class FLineSampleContext(val requestedCount: Int = 100, val seed: Long? = null, val pointSetAware: Boolean = true)

Sampling request context for FLineSampler.

Declaration Summary
val requestedCount: Int
val seed: Long?
val pointSetAware: Boolean
override fun toString(): String

FLineSampler

fun interface FLineSampler

Composable sampler interface for extracting point-series data from an FLine.

Declaration Summary
fun sample(line: FLine, context: FLineSampleContext): List<FLineSample>

FLineSamplers

object FLineSamplers

Built-in sampler factories.

Declaration Summary
fun uniform(): FLineSampler Uniform parameter sampling (equivalent to legacy sampleByNumber for non point-set lines).
fun points(): FLineSampler Return instruction points (node endpoints) in order.
fun byDistance(spacing: Float): FLineSampler Arc-length sampling by fixed spacing. For point-set lines this returns point nodes.
fun random(stratified: Boolean = true, includeEnds: Boolean = true, seed: Long? = null): FLineSampler Random parameter sampling (optionally stratified). Endpoints are included by default.
fun curvatureAdaptive(curvaturePower: Float = 1f, gain: Float = 2f, oversample: Int = 4, minBaseSamples: Int = 32): FLineSampler Curvature-adaptive sampling: spends more samples where turning is higher.
fun douglasPeucker(epsilon: Float, denseOversample: Int = 4, minDenseSamples: Int = 64): FLineSampler Ramer-Douglas-Peucker simplification on a dense sample.
fun patterned(pattern: List<Float>, includeEnds: Boolean = true): FLineSampler Patterned spacing with repeating interval weights (e.g. [1,2,1,3]).
fun compose(vararg samplers: FLineSampler): FLineSampler Compose several samplers by applying each to the output polyline of the previous.

FLineSerialization.kt

Serializes an FLine to JSON and back, nodes and properties included, through FLineSerializer.

FLine names FLineSerializer in its @Serializable annotation, so any kotlinx.serialization format picks it up. Each property value is written with a type tag: numbers, booleans, strings and Vec2/Vec3/Vec4. A value of any other type throws SerializationException; a few runtime-only properties (onClick callbacks, thicken strokes) are skipped instead.

src/jvmMain/kotlin/afield/geometry/path/FLineSerialization.kt — read the source

FLineSerializer

object FLineSerializer : KSerializer<FLine>

Declaration Summary
override val descriptor: SerialDescriptor
override fun serialize(encoder: Encoder, value: FLine)
override fun deserialize(decoder: Decoder): FLine

FLineThickening.kt

Turning a line into an outline: offsets, ribbons and buffers of an FLine, at fixed or varying width.

byRibboning and byVariableRibboning return an FLineRibbon with left and right edges and a closed shell. byJavaOffsetting uses Java2D strokes; byJtsBuffering, byJtsOffsetting and byJtsVariableBuffering use JTS, with FLineStrokeJoin and FLineStrokeCap for the corner and end style. byJtsSplitting separates a self-intersecting line into an FLineSplit.

src/jvmMain/kotlin/afield/geometry/path/FLineThickening.kt — read the source

Top-level functions

Declaration Summary
fun FLine.byBidirectionalOffsetting(distance: Float, samples: Int = 200, sampler: FLineSampler? = null): Pair<FLine, FLine> Create left/right offset edges at equal distance from the centerline.
fun FLine.byVariableThicknessOffsetting(leftDistance: ParameterFunction, rightDistance: ParameterFunction = leftDistance, samples: Int = 200, sampler: FLineSampler? = null): Pair<FLine, FLine> Create left/right offset edges with independent distance functions.
fun FLine.byRibboning(thickness: Float, samples: Int = 200, sampler: FLineSampler? = null): FLine Build a constant-thickness ribbon polygon around this line.
fun FLine.byVariableRibboning(thicknessFunc: ParameterFunction, samples: Int = 200, sampler: FLineSampler? = null): FLine Build a variable-thickness ribbon polygon around this line.
fun FLine.byJavaOffsetting(strokeWidth: Float, cap: FLineStrokeCap = FLineStrokeCap.ROUND, join: FLineStrokeJoin = FLineStrokeJoin.ROUND, miterLimit: Float = 10f): FLine Java2D stroke-based thickening using BasicStroke; good quality for 2D cases.
fun FLine.byJtsBuffering(distance: Double, flatness: Double = 0.5, quadrantSegments: Int = 8, join: FLineStrokeJoin = FLineStrokeJoin.ROUND, cap: FLineStrokeCap = FLineStrokeCap.ROUND, miterLimit: Double = 5.0): FLine JTS buffer for this line (2D), returning filled geometry as an FLine.
fun FLine.byJtsOffsetting(distance: Double, flatness: Double = 0.5): FLine JTS offset-curve extraction (2D).
fun FLine.byJtsVariableBuffering(numSamples: Int = 128, distance: (Double) -> Double, sampler: FLineSampler? = null, flatness: Double = 0.5): FLine JTS variable buffer sampled from this curve.
fun FLine.byJtsSplitting(flatness: Double = 0.5): FLineSplit Split potentially self-crossing geometry into loops and open lines using JTS noding/polygonization.

FLineStrokeJoin

enum class FLineStrokeJoin(internal val awtJoin: Int, internal val jtsJoin: Int)

Join style for Java2D/JTS offset and buffer operations.

Constants: MITER, ROUND, BEVEL.

FLineStrokeCap

enum class FLineStrokeCap(internal val awtCap: Int, internal val jtsCap: Int)

Cap style for Java2D/JTS offset and buffer operations.

Constants: BUTT, ROUND, SQUARE.

FLineRibbon

data class FLineRibbon(val left: FLine, val right: FLine, val shell: FLine)

Ribbon result split into its left edge, right edge, and closed shell.

Declaration Summary
val left: FLine
val right: FLine
val shell: FLine
override fun toString(): String

FLineSplit

data class FLineSplit(val outerLoop: FLine?, val innerLoops: List<FLine>, val lines: List<FLine>)

JTS split result: largest outer loop, interior loops, and open line components.

Declaration Summary
val outerLoop: FLine?
val innerLoops: List<FLine>
val lines: List<FLine>
override fun toString(): String

JtsCurveSplitter

class JtsCurveSplitter(private val flatness: Double = 0.5)

JTS helper for splitting/flattening/shape conversion.

Declaration Summary
fun split(line: FLine): FLineSplit

FLineWiggle.kt

Wiggling one line along another, and the moving frames that make it possible.

wiggledBy reads a second line's coordinates in a frame travelling along this one, so a small wave becomes a decoration along a path. sweptFrames returns the SweptFrames (position, tangent, normal, binormal) it uses, for placing other things along a line. Tests: FLineWiggleTest.

src/jvmMain/kotlin/afield/geometry/path/FLineWiggle.kt — read the source

Used in: FLine examples (Field graphics)

Top-level functions

Declaration Summary
fun FLine.wiggledBy(a: FLine, repeats: Int = 1, subtractChord: Boolean = true, amplitude: Number = 1.0, resolution: Int = 200, up: Vec3? = Vec3(0f, 0f, 1f)): FLine Wiggle this line with another: a's coordinates are read in a frame that travels along this line.
fun FLine.sweptFrames(samples: Int = 400, up: Vec3? = Vec3(0f, 0f, 1f)): SweptFrames? Frames along this line; see SweptFrames. null if the line has no length.

SweptFrame

class SweptFrame(val position: DoubleArray, val tangent: DoubleArray, val normal: DoubleArray, val binormal: DoubleArray)

One frame along a line: where it is, and the tangent / normal / binormal there.

SweptFrames

class SweptFrames internal constructor(val positions: Array<DoubleArray>, val tangents: Array<DoubleArray>, val normals: Array<DoubleArray>, val binormals: Array<DoubleArray>, val fractions: DoubleArray, val planar: Boolean, val strength: Double, val length: Double)

Frames swept along a line, sampled evenly by arc length. Query any point with frameAt.

Declaration Summary
val positions: Array<DoubleArray>
val tangents: Array<DoubleArray>
val normals: Array<DoubleArray>
val binormals: Array<DoubleArray>
val fractions: DoubleArray Arc-length fraction (0..1) of each sample.
val planar: Boolean
val strength: Double
val length: Double Arc length of the line.
fun frameAt(s: Double): SweptFrame The frame at arc-length fraction s, interpolated between samples. Past either end the line is continued straight along its end tangent, with the end frame.

FloatEarcut.kt

src/jvmMain/kotlin/afield/geometry/path/FloatEarcut.kt — read the source

FloatEarcut

object FloatEarcut

Declaration Summary
fun earcut(data: FloatArray, holeIndices: IntArray? = null, dim: Int = 2): MutableList<Int> Triangulates the given polygon

Hyperbezier.kt

The hyperbezier curve family, ported from Raph Levien's hyperbezier (linebender.org/blog/hyperbezier/, August 2026); fitHyperbezierCubics is what draws one into an FLine.

A hyperbezier is the plane curve traced at unit speed whose curvature is a rational function of arc length s:

    kappa(s) = (a*s + b) / q(s)^1.5,     q(s) = c*s^2 + d*s + 1

over s in [0, 1], then similarity-transformed so it runs from (0, 0) to (1, 0), which is what the rest of this file calls the chord frame. q is a dial between two regimes a single cubic has to compromise between: q constant gives circular arcs and Euler spirals exactly, while q dipping towards zero gives a corner-like curvature spike. The family also contains the log-aesthetic curves and the circle involute exactly. Its curvature numerator is linear in s, so it has at most one inflection, and that is what it gives up relative to a cubic: following a cubic with two inflections takes more than one hyperbezier.

theta(s), the integral of kappa, has a closed form. The position integral of e^(i*theta) does not, being Fresnel-like, so that part is done numerically with Gauss-Legendre quadrature.

Construct a curve from a cubic-shaped control polygon with hyperbezierFor; it is rarely useful to pick a, b, c and d by hand.

src/jvmMain/kotlin/afield/geometry/path/Hyperbezier.kt — read the source

Top-level functions

Declaration Summary
fun circleArm(angle: Double): Double The "parabola rule" arm length for a tangent that leaves the chord at angle radians: the arm at which a cubic's midpoint coincides exactly with a circular arc's apex, at every angle. Arms are measured against this, so an arm of circleArm(theta) means "circle-like", and hyperbezierFor returns an exact arc or Euler spiral there.
fun hyperbezierFor(th0: Double, th1: Double, l0: Double, l1: Double, cornerBias: Double = 1.0, armResponse: Double = 2.0): Hyperbezier? The control-polygon to hyperbezier map: turn a cubic-Bezier-shaped control polygon, expressed in the chord frame, into the curve that replaces it.

Hyperbezier

class Hyperbezier(val a: Double, val b: Double, val c: Double, val d: Double)

One hyperbezier, as the four coefficients of its curvature function, evaluated in the chord frame.

Declaration Summary
val a: Double Linear coefficient of the curvature numerator.
val b: Double Constant coefficient of the curvature numerator.
val c: Double Quadratic coefficient of the denominator q.
val d: Double Linear coefficient of the denominator q.
val chordScale: Double Length of the endpoint integral. Everything in the chord frame is the raw curve scaled by 1 / chordScale, so raw curvatures multiply by this and raw lengths divide by it.
val isEulerFamily: Boolean True when q is constant, i.e. the curve is exactly a circular arc (a == 0) or an Euler spiral. Control polygons whose arms are exactly the "parabola rule" length circleArm land here at every angle.
val arcLength: Double Total arc length of the curve in the chord frame, where the chord itself has length 1.
fun q(t: Double): Double The denominator quadratic q(t) = c*t^2 + d*t + 1.
fun pointAt(t: Double): DoubleArray Position at arc-length fraction t in the chord frame: pointAt(0) is (0, 0) and pointAt(1) is (1, 0), to within the accuracy of the quadrature. Returned as doubleArrayOf(x, y) — the caller converts to Field's single-precision Vec2/Vec3 once, at the end, rather than losing digits at every step of a fit.
fun tangentAngleAt(t: Double): Double Tangent angle at arc-length fraction t, in radians, measured counter-clockwise from the chord. tangentAngleAt(0) and tangentAngleAt(1) reproduce the th0 and th1 handed to hyperbezierFor — that G1 interpolation is what the solver is solving for.
fun curvatureAt(t: Double): Double Signed curvature at arc-length fraction t, in the chord frame.
fun featureParameters(): List<Double> Parameters strictly inside (0, 1) that are natural places to split the curve when approximating it: the minimum of q — the corner, where curvature spikes — and the extrema of curvature. Sorted ascending.
override fun toString(): String

LowSegmentFLineFitting.kt

Fits a point sequence with as few line and arc segments as its error budget allows.

fitLowSegmentFLine returns the fitted FLine, and fitLowSegmentFLineResult also reports the error in a LowSegmentFLineFitResult; LowSegmentFLineFitConfig sets the segment limit and the improvement a split must earn. Tests: LowSegmentFLineFittingTest.

src/jvmMain/kotlin/afield/geometry/path/LowSegmentFLineFitting.kt — read the source

Top-level functions

Declaration Summary
fun lowSegmentFLine(points: Iterable<Vec3>, config: LowSegmentFLineFitConfig = LowSegmentFLineFitConfig()): FLine
fun Iterable<Vec3>.fitLowSegmentFLine(config: LowSegmentFLineFitConfig = LowSegmentFLineFitConfig()): FLine
fun Iterable<Vec3>.fitLowSegmentFLineResult(config: LowSegmentFLineFitConfig = LowSegmentFLineFitConfig()): LowSegmentFLineFitResult

LowSegmentFLineFitConfig

data class LowSegmentFLineFitConfig(val maxSegments: Int = 3, val minSplitRelativeImprovement: Double = 0.30, val minSplitAbsoluteImprovement: Double = 0.02, val complexityPenalty: Double = 0.015, val minArcRelativeImprovement: Double = 0.15, val minArcAbsoluteImprovement: Double = 0.005, val maxArcSweep: Double = PI * 1.75, val maxArcSweepPerCubic: Double = PI / 2.0, val maxArcRadiusToChordRatio: Double = 1_000.0, val arcCandidateCount: Int = 17)

Controls the conservative low-segment fitter used by fitLowSegmentFLine.

Declaration Summary
val maxSegments: Int Most primitives the fit may use; also capped at one fewer than the number of input points.
val minSplitRelativeImprovement: Double An extra segment must cut the RMS error by at least this fraction of the current error.
val minSplitAbsoluteImprovement: Double And by at least this much outright — as a fraction of the points' bounding-box diagonal (floored at 1), not as a distance in the input's own units.
val complexityPenalty: Double Charged per primitive beyond the first when comparing two fits, in the same bounding-box diagonal units: the cheaper fit wins even when both pass the improvement tests.
val minArcRelativeImprovement: Double An arc must beat the straight line's RMS error by at least this fraction of it.
val minArcAbsoluteImprovement: Double And by at least this much outright, in bounding-box diagonal units as above.
val maxArcSweep: Double Largest sweep a single arc primitive may cover, radians. Clamped to just under a full turn.
val maxArcSweepPerCubic: Double Largest sweep carried by one cubic when the arc is written out; longer arcs are split into that many cubics. Clamped to at most π.
val maxArcRadiusToChordRatio: Double Rejects the near-straight arcs: drops any candidate whose circle centre would sit farther from the chord than this multiple of the chord's length.
val arcCandidateCount: Int How many candidate arc heights to score per chord; at least 3.

LowSegmentFLineFitResult

data class LowSegmentFLineFitResult(val line: FLine, val rmsError: Double, val maxError: Double, val segmentCount: Int, val primitives: List<LowSegmentFLineFitPrimitive>)

Declaration Summary
val line: FLine
val rmsError: Double
val maxError: Double
val segmentCount: Int
val primitives: List<LowSegmentFLineFitPrimitive>

LowSegmentFLineFitPrimitive

sealed class LowSegmentFLineFitPrimitive

Declaration Summary
abstract val startIndex: Int
abstract val endIndex: Int
abstract val start: Vec3
abstract val end: Vec3
abstract val rmsError: Double
abstract val maxError: Double
data class Line(override val startIndex: Int, override val endIndex: Int, override val start: Vec3, override val end: Vec3, override val rmsError: Double, override val maxError: Double) : LowSegmentFLineFitPrimitive()
data class Arc(override val startIndex: Int, override val endIndex: Int, override val start: Vec3, override val end: Vec3, override val rmsError: Double, override val maxError: Double, val center: Vec3, val radius: Double, val sweep: Double) : LowSegmentFLineFitPrimitive()

PathFlattener.kt

Converts between arc length along an FLine and its node parameter, by flattening its curves to a tolerance.

src/jvmMain/kotlin/afield/geometry/path/PathFlattener.kt — read the source

PathFlattener

class PathFlattener(private val c: FLine, tol: Float)

Flattens a line to tolerance tol; lengthToDot turns a distance along it into a node parameter and dotToLength goes back.

Declaration Summary
inner class Mapping
var mappings: MutableList<Mapping>
var searchDistance: Comparator<Any>
var searchDot: Comparator<Any>
fun length(): Float
fun getMappingSublist(length: Float): List<Mapping>
fun lengthToDot(length: Float): Float
fun dotToLength(dot: Float): Float
var tmp: Vec3

PlanarTessellation.kt

Fills sets of 3D rings, holes included, by grouping them into planes and handing each plane to FloatEarcut.

PlanarTessellation.tessellate returns PlanarTessellation.Polygons whose triangles refer back into the input rings, and Coplanarity sets how close two rings must be to share a plane. Tessellation commutes with rigid rotation, which TessellationOrientationOracleTest checks.

src/jvmMain/kotlin/afield/geometry/path/PlanarTessellation.kt — read the source

Coplanarity

data class Coplanarity(val sineOfAngle: Float = 1e-3f, val offsetFraction: Float = 1e-3f)

How close two rings have to be before PlanarTessellation treats them as lying in the same plane.

Declaration Summary
val sineOfAngle: Float
val offsetFraction: Float

PlanarTessellation

object PlanarTessellation

The one triangulator for filled, arbitrarily oriented, hole-carrying ring sets.

Declaration Summary
data class Vertex(val ring: Int, val vertex: Int) A reference to one point of one input ring: ringsring``.
class Polygon internal constructor(val outer: Int, val holes: List<Int>, val triangles: List<Vertex>, val normal: Vec3) One filled polygon: an outer ring, the coplanar rings subtracted from it, and its triangles.
fun newellNormal(ring: List<Vec3>): Vec3 Newell's normal of a ring, unnormalised — its length is twice the ring's planar area, which is what makes it usable both as a direction and as a measure of how much to trust that direction.
fun tessellate(rings: List<List<Vec3>>, coplanarity: Coplanarity = Coplanarity.DEFAULT): List<Polygon> Triangulate rings — each an ordered, closed, already-flattened ring, with no repeated final point — grouping them into planes and holes as described on PlanarTessellation.

PolynomialTreeFitting.kt

Fits a List<Vec3> or an FLine with a tree of 3D polynomial curves, splitting a span until each leaf meets its error targets.

The fitPolynomialTree3D extensions take a PolynomialTreeFitConfig (polynomial order, depth and leaf limits, RMS and max-error targets) and returns a PolynomialTreeFitResult. Split, stop and claim rules are pluggable through PolynomialTreeSplitStrategies, PolynomialTreeStopCriteria and PolynomialTreeClaimCriteria; PolynomialLeafFLineStyles holds preset styles for drawing the fitted leaves as FLines.

src/jvmMain/kotlin/afield/geometry/path/PolynomialTreeFitting.kt — read the source

Top-level functions

Declaration Summary
fun PolynomialTreeNode.levelTraversal(controller: PolynomialTreeTraversalController = PolynomialTreeTraversalController { PolynomialTreeTraversalDecision() }): Sequence<PolynomialTreeTraversalLevel>
fun PolynomialTreeNode.approxCurveLength(samples: Int = 32): Double
fun fitPolynomialTree3DFromPoints(points: List<Vec3>, config: PolynomialTreeFitConfig = PolynomialTreeFitConfig()): PolynomialTreeFitResult Fit a recursive (possibly unbalanced) tree of 3D polynomial segments to ordered points.
fun List<Vec3>.fitPolynomialTree3D(config: PolynomialTreeFitConfig = PolynomialTreeFitConfig()): PolynomialTreeFitResult
fun FLine.fitPolynomialTree3D(config: PolynomialTreeFitConfig = PolynomialTreeFitConfig(), samples: Int = 128, sampler: FLineSampler? = null): PolynomialTreeFitResult Convenience wrapper for fitting a polynomial tree to points sampled from an FLine.
fun PolynomialTreeNode.toFLine(samples: Int = 64): FLine
fun PolynomialTreeFitResult.toIncrementalUpdater(points: List<Vec3> = inputPoints): PolynomialTreeIncrementalUpdater
fun List<Vec3>.fitPolynomialTree3DIncremental(config: PolynomialTreeFitConfig = PolynomialTreeFitConfig()): PolynomialTreeIncrementalUpdater
fun FLine.fitPolynomialTree3DIncremental(config: PolynomialTreeFitConfig = PolynomialTreeFitConfig(), samples: Int = 128, sampler: FLineSampler? = null): PolynomialTreeIncrementalUpdater

Polynomial3Curve

data class Polynomial3Curve(val xCoefficients: DoubleArray, val yCoefficients: DoubleArray, val zCoefficients: DoubleArray)

Parametric 3D polynomial curve: x(t), y(t), z(t) for t in [0, 1].

Declaration Summary
val xCoefficients: DoubleArray
val yCoefficients: DoubleArray
val zCoefficients: DoubleArray
val order: Int
fun evaluate(t: Double): Vec3+1 overload · also Float
fun sample(samples: Int = 64, tStart: Double = 0.0, tEnd: Double = 1.0): List<Vec3>
fun toFLine(samples: Int = 64, tStart: Double = 0.0, tEnd: Double = 1.0): FLine

PolynomialTreeNode

data class PolynomialTreeNode(val id: Int, val depth: Int, val sourceRange: IntRange, val supportIndices: IntArray, val polynomial: Polynomial3Curve, val supportRmsError: Double, val supportMeanError: Double, val supportMaxError: Double, val splitPointIndex: Int? = null, val stopReason: String, val claimedIndices: IntArray = IntArray(0), val claimedRmsError: Double? = null, val claimedMaxError: Double? = null, val left: PolynomialTreeNode? = null, val right: PolynomialTreeNode? = null)

Declaration Summary
val id: Int
val depth: Int
val sourceRange: IntRange
val supportIndices: IntArray
val polynomial: Polynomial3Curve
val supportRmsError: Double
val supportMeanError: Double
val supportMaxError: Double
val splitPointIndex: Int?
val stopReason: String
val claimedIndices: IntArray
val claimedRmsError: Double?
val claimedMaxError: Double?
val left: PolynomialTreeNode?
val right: PolynomialTreeNode?
val isLeaf: Boolean
fun leaves(): List<PolynomialTreeNode>

PolynomialTreeTraversalContext

data class PolynomialTreeTraversalContext(val node: PolynomialTreeNode, val depth: Int, val indexInLevel: Int, val parent: PolynomialTreeNode?)

Declaration Summary
val node: PolynomialTreeNode
val depth: Int
val indexInLevel: Int
val parent: PolynomialTreeNode?

PolynomialTreeTraversalDecision

data class PolynomialTreeTraversalDecision(val emit: Boolean = true, val descend: Boolean = true, val stopAll: Boolean = false)

Declaration Summary
val emit: Boolean
val descend: Boolean
val stopAll: Boolean

PolynomialTreeTraversalLevel

data class PolynomialTreeTraversalLevel(val depth: Int, val nodes: List<PolynomialTreeTraversalContext>)

Declaration Summary
val depth: Int
val nodes: List<PolynomialTreeTraversalContext>
fun toFLines(samplesPerNode: Int = 64): List<FLine>
fun toFLines(mapper: (PolynomialTreeTraversalContext) -> FLine?): List<FLine>

PolynomialTreeTraversalController

fun interface PolynomialTreeTraversalController

Declaration Summary
fun decide(context: PolynomialTreeTraversalContext): PolynomialTreeTraversalDecision

PolynomialTreeFitResult

data class PolynomialTreeFitResult(val inputPoints: List<Vec3>, val config: PolynomialTreeFitConfig, val root: PolynomialTreeNode, val leaves: List<PolynomialTreeNode>, val pointErrors: DoubleArray, val nearestLeafIds: IntArray, val overallRmsError: Double, val overallMeanError: Double, val overallMaxError: Double)

Declaration Summary
val inputPoints: List<Vec3>
val config: PolynomialTreeFitConfig
val root: PolynomialTreeNode
val leaves: List<PolynomialTreeNode>
val pointErrors: DoubleArray
val nearestLeafIds: IntArray
val overallRmsError: Double
val overallMeanError: Double
val overallMaxError: Double
fun leafById(id: Int): PolynomialTreeNode?
fun levelTraversal(controller: PolynomialTreeTraversalController = PolynomialTreeTraversalController { PolynomialTreeTraversalDecision() }): Sequence<PolynomialTreeTraversalLevel>
fun toLeafFLines(samplesPerLeaf: Int = 64): List<FLine>
fun toLeafFLines(render: PolynomialLeafFLineRenderConfig): List<FLine>
fun toLeafFLineMap(samplesPerLeaf: Int = 64): Map<String, List<FLine>>
fun toLeafFLineMap(render: PolynomialLeafFLineRenderConfig, keyPrefix: String = "poly_leaf"): Map<String, List<FLine>>
fun incrementalUpdater(points: List<Vec3> = inputPoints): PolynomialTreeIncrementalUpdater

PolynomialLeafFLineOutputMode

enum class PolynomialLeafFLineOutputMode

Constants: SEPARATE_LINES, SINGLE_MULTIPATH, SINGLE_CONNECTED.

PolynomialLeafOrderMode

enum class PolynomialLeafOrderMode

Constants: SOURCE_ASCENDING, SOURCE_DESCENDING, ERROR_DESCENDING, ERROR_ASCENDING, DEPTH_ASCENDING, DEPTH_DESCENDING.

PolynomialLeafGapStyle

data class PolynomialLeafGapStyle(val enabled: Boolean = false, val absolute: Double = 0.0, val fractionOfSpan: Double = 0.06, val trimStart: Boolean = true, val trimEnd: Boolean = true, val minAbsolute: Double = 0.0, val maxAbsolute: Double = Double.POSITIVE_INFINITY)

Declaration Summary
val enabled: Boolean
val absolute: Double
val fractionOfSpan: Double
val trimStart: Boolean
val trimEnd: Boolean
val minAbsolute: Double
val maxAbsolute: Double

PolynomialLeafOvershootStyle

data class PolynomialLeafOvershootStyle(val enabled: Boolean = false, val startAbsolute: Double = 0.0, val endAbsolute: Double = 0.0, val startFractionOfSpan: Double = 0.0, val endFractionOfSpan: Double = 0.08, val maxAbsolute: Double = Double.POSITIVE_INFINITY)

Declaration Summary
val enabled: Boolean
val startAbsolute: Double
val endAbsolute: Double
val startFractionOfSpan: Double
val endFractionOfSpan: Double
val maxAbsolute: Double

PolynomialLeafSketchStyle

data class PolynomialLeafSketchStyle(val passes: Int = 1, val jitterAmplitude: Double = 0.0, val alongTangentFactor: Double = 0.25, val keepEndpoints: Boolean = true, val seed: Long = 1L)

Declaration Summary
val passes: Int
val jitterAmplitude: Double
val alongTangentFactor: Double
val keepEndpoints: Boolean
val seed: Long

PolynomialLeafFLineRenderConfig

data class PolynomialLeafFLineRenderConfig(val samplesPerLeaf: Int = 64, val outputMode: PolynomialLeafFLineOutputMode = PolynomialLeafFLineOutputMode.SEPARATE_LINES, val orderMode: PolynomialLeafOrderMode = PolynomialLeafOrderMode.SOURCE_ASCENDING, val gap: PolynomialLeafGapStyle = PolynomialLeafGapStyle.none(), val overshoot: PolynomialLeafOvershootStyle = PolynomialLeafOvershootStyle.none(), val sketch: PolynomialLeafSketchStyle = PolynomialLeafSketchStyle())

Declaration Summary
val samplesPerLeaf: Int
val outputMode: PolynomialLeafFLineOutputMode
val orderMode: PolynomialLeafOrderMode
val gap: PolynomialLeafGapStyle
val overshoot: PolynomialLeafOvershootStyle
val sketch: PolynomialLeafSketchStyle

PolynomialLeafFLineStyles

object PolynomialLeafFLineStyles

Declaration Summary
fun clean(samplesPerLeaf: Int = 64, outputMode: PolynomialLeafFLineOutputMode = PolynomialLeafFLineOutputMode.SEPARATE_LINES): PolynomialLeafFLineRenderConfig
fun gapped(samplesPerLeaf: Int = 64, gapAbsolute: Double = 0.0, gapFractionOfSpan: Double = 0.08, outputMode: PolynomialLeafFLineOutputMode = PolynomialLeafFLineOutputMode.SEPARATE_LINES): PolynomialLeafFLineRenderConfig
fun overshooting(samplesPerLeaf: Int = 64, overshootAbsolute: Double = 0.0, overshootFractionOfSpan: Double = 0.1, outputMode: PolynomialLeafFLineOutputMode = PolynomialLeafFLineOutputMode.SEPARATE_LINES): PolynomialLeafFLineRenderConfig
fun sketchy(samplesPerLeaf: Int = 72, passes: Int = 3, jitterAmplitude: Double = 0.25, gapFractionOfSpan: Double = 0.05, overshootFractionOfSpan: Double = 0.09, outputMode: PolynomialLeafFLineOutputMode = PolynomialLeafFLineOutputMode.SEPARATE_LINES): PolynomialLeafFLineRenderConfig

PolynomialTreeSplitCriterion

fun interface PolynomialTreeSplitCriterion

Declaration Summary
fun chooseSplit(context: PolynomialTreeSplitContext): Int?

PolynomialTreeStopCriterion

fun interface PolynomialTreeStopCriterion

Declaration Summary
fun shouldStop(context: PolynomialTreeStopContext): Boolean

PolynomialTreeClaimCriterion

fun interface PolynomialTreeClaimCriterion

Declaration Summary
fun accepts(context: PolynomialTreeClaimContext): Boolean

PolynomialTreeSplitContext

data class PolynomialTreeSplitContext(val points: List<Vec3>, val depth: Int, val supportIndices: IntArray, val supportRange: IntRange, val pointErrors: DoubleArray, val nearestT: DoubleArray, val rmsError: Double, val meanError: Double, val maxError: Double, val maxErrorLocalIndex: Int)

Declaration Summary
val points: List<Vec3>
val depth: Int
val supportIndices: IntArray
val supportRange: IntRange
val pointErrors: DoubleArray
val nearestT: DoubleArray
val rmsError: Double
val meanError: Double
val maxError: Double
val maxErrorLocalIndex: Int
val size: Int
fun middleLocalIndex(): Int
fun globalIndexAt(localIndex: Int): Int
fun localIndexOf(globalPointIndex: Int): Int

PolynomialTreeStopContext

data class PolynomialTreeStopContext(val split: PolynomialTreeSplitContext, val config: PolynomialTreeFitConfig)

Declaration Summary
val split: PolynomialTreeSplitContext
val config: PolynomialTreeFitConfig

PolynomialTreeClaimContext

data class PolynomialTreeClaimContext(val pointIndex: Int, val point: Vec3, val leafId: Int, val leafRange: IntRange, val distance: Double, val rankByDistance: Int, val ownerLeafId: Int, val ownerDistance: Double, val bestDistance: Double)

Declaration Summary
val pointIndex: Int
val point: Vec3
val leafId: Int
val leafRange: IntRange
val distance: Double
val rankByDistance: Int
val ownerLeafId: Int
val ownerDistance: Double
val bestDistance: Double

PolynomialTreeClaimConfig

data class PolynomialTreeClaimConfig(val enabled: Boolean = false, val criterion: PolynomialTreeClaimCriterion = PolynomialTreeClaimCriteria.distanceBelow(0.75), val includeSequentialOwner: Boolean = false, val maxClaimsPerPoint: Int = 1, val maxClaimsPerLeaf: Int = Int.MAX_VALUE, val refitLeaves: Boolean = false, val claimRefitWeight: Double = 0.35)

Declaration Summary
val enabled: Boolean
val criterion: PolynomialTreeClaimCriterion
val includeSequentialOwner: Boolean
val maxClaimsPerPoint: Int
val maxClaimsPerLeaf: Int
val refitLeaves: Boolean
val claimRefitWeight: Double

PolynomialTreeFitConfig

data class PolynomialTreeFitConfig(val order: Int = 3, val maxDepth: Int = 8, val maxLeaves: Int = 64, val minPointsPerLeaf: Int = order + 2, val targetRmsError: Double = 1.0, val targetMaxError: Double = 2.0, val splitCriterion: PolynomialTreeSplitCriterion = PolynomialTreeSplitStrategies.maxError(), val stopCriterion: PolynomialTreeStopCriterion = PolynomialTreeStopCriteria.byThresholds, val distanceSamples: Int = 48, val distanceRefinementSteps: Int = 8, val regularization: Double = 1e-8, val claims: PolynomialTreeClaimConfig = PolynomialTreeClaimConfig())

Declaration Summary
val order: Int
val maxDepth: Int
val maxLeaves: Int
val minPointsPerLeaf: Int
val targetRmsError: Double
val targetMaxError: Double
val splitCriterion: PolynomialTreeSplitCriterion
val stopCriterion: PolynomialTreeStopCriterion
val distanceSamples: Int
val distanceRefinementSteps: Int
val regularization: Double
val claims: PolynomialTreeClaimConfig

PolynomialTreeSplitStrategies

object PolynomialTreeSplitStrategies

Declaration Summary
fun maxError(): PolynomialTreeSplitCriterion
fun midpoint(): PolynomialTreeSplitCriterion
fun hybrid(errorBias: Double = 0.7): PolynomialTreeSplitCriterion
fun curvatureAware(errorWeight: Double = 0.65, curvatureWeight: Double = 0.35): PolynomialTreeSplitCriterion

PolynomialTreeStopCriteria

object PolynomialTreeStopCriteria

Declaration Summary
val byThresholds
fun maxErrorOnly(maxError: Double): PolynomialTreeStopCriterion
fun rmsOnly(rmsError: Double): PolynomialTreeStopCriterion
val never

PolynomialTreeClaimCriteria

object PolynomialTreeClaimCriteria

Declaration Summary
fun distanceBelow(maxDistance: Double): PolynomialTreeClaimCriterion
fun closeToBest(maxDistance: Double = 1.0, ratioFromBest: Double = 1.2): PolynomialTreeClaimCriterion
fun betterThanOwner(maxDistance: Double = 1.0, minImprovement: Double = 0.25): PolynomialTreeClaimCriterion

PolynomialTreeIncrementalSummary

data class PolynomialTreeIncrementalSummary(val pointCount: Int, val overallRmsError: Double, val overallMeanError: Double, val overallMaxError: Double)

Declaration Summary
val pointCount: Int
val overallRmsError: Double
val overallMeanError: Double
val overallMaxError: Double

PolynomialTreeIncrementalUpdater

class PolynomialTreeIncrementalUpdater internal constructor(private val config: PolynomialTreeFitConfig, templateRoot: PolynomialTreeNode, initialPoints: List<Vec3>)

Mutable updater that keeps a fixed tree topology and reusable buffers while refitting against new points.

Declaration Summary
var overallRmsError: Double
var overallMeanError: Double
var overallMaxError: Double
val pointCount: Int
val leafCount: Int
fun summary(): PolynomialTreeIncrementalSummary
fun pointErrorsCopy(): DoubleArray
fun nearestLeafIdsCopy(): IntArray
fun snapshot(): PolynomialTreeFitResult
fun update(newPoints: List<Vec3>): PolynomialTreeIncrementalSummary

contours.kt

Marching-squares iso-contours for 2D scalar fields, plus the scalar field most callers want to contour.

A field is just a ScalarField2D, (Double, Double) -> Double. pointEdgeScalarField2D builds one from points and edges — or from a 2D FLine — as the union of weighted discs and capsules, signed so that iso = 0.0 is the tube boundary; it re-reads the supplied point list on every sample, so mutating those Vec2s moves the field live. generateIsoContours samples a field on an nx by ny grid and returns stitched polylines, generateIsoContoursWithStep takes a target cell size instead, and the FLine overloads choose the sampling window from the line's 2D bounds grown by the tube radius.

smoothIsoContours and generateSmoothIsoContours fit cubic splines through the polylines, treating a contour that ends on the sampling bounds as open and the rest as closed loops.

Varying the radius along the line — weights

A tube of one radius is pointRadius/edgeRadius. To make it taper, weight the primitives: every function that builds a point/edge field takes a weight callback rather than a list of weights. On an explicit graph that is PointEdgeWeight, (index, point) -> Double, indexed the same way as the points you passed in. On an FLine it is FLinePointWeight, (nodeIndex, point) -> Double, where nodeIndex is the fractional drawing-instruction index of the flattened point — integral at a moveTo/lineTo, and interpolated across a cubicTo, which is the same quantity FLine.uniform_linearize hands its callback and linesOut uses to look up per-node colour. FLine.isoWeights is the stock implementation: it reads a per-node property and interpolates between the nodes that set it.

It is a callback rather than a list of weights because on the FLine overloads there is no count a caller could size a list against: the points and edges belong to pointEdgeGraph2D's internal flattening, and its edge count depends on the line's geometry as well as its instructions.

An edge's weight is always the minimum of its two endpoints'. A capsule has one radius for its whole length and so cannot taper: the taper comes from the union of densely sampled discs, and the edges only bridge the gaps between them. Taking the maximum would let a capsule bulge past the disc envelope exactly where the weight changes fastest; the minimum fills the gap without ever fattening it.

The sign convention

A grid corner is inside iff its sample is strictly below iso; a sample exactly at iso counts as outside. The test is two-valued rather than three-way, because under a three-way test — below at one end of an edge, above at the other, so "equal" is neither — a sample sitting exactly on iso suppresses the crossing on every edge out of its corner, and the contour vanishes or shatters into arcs. Samples land exactly on iso for ordinary input: any grid that samples a linear field at its own zero level puts a whole column there, and a unit circle contoured at iso = 0 passes through a grid corner at plenty of resolutions, nx = ny = 16 over [-2, 2] among them.

A crossing that lands on a corner is pushed off it by ISO_CROSSING_EPSILON, which moves it by at most a thousandth of the cell edge it lies on — an analytic bound, since it is a coerceIn on the interpolation parameter. marchingCubes in mesh_sdf.kt is the 3D marcher and uses the same convention for the same reason.

The saddle cell

A cell with four crossings is resolved by the asymptotic decider: the sign of the cell's bilinear saddle value decides which diagonal pair of corners the centre of the cell belongs to. It is a pure function of the cell's own four corner values, so two cells sharing an edge cannot disagree about the crossing on it, which is what the stitcher needs. The ambiguity itself does not go away: when the saddle value is exactly iso the two pairings are equally correct and the decider has nothing to go on. SADDLE_TIE_RELATIVE_EPSILON settles that case in the code rather than leaving it to the direction a multiplication rounded — which matters because every bilinear field lands there exactly.

Stitching and winding

Each crossing is computed once and keyed by its grid edge, as marchingCubes keys its shared vertices, so segments are chained by key in time linear in their number. Every segment is oriented with the inside on its left, so a closed contour comes back counter-clockwise around an inside region and clockwise around a hole in one: summed signed areas give the inside's true area.

src/jvmMain/kotlin/afield/geometry/path/contours.kt — read the source

Used in: Points into structure — Voronoi, Apollonius, contours and clusters

Top-level functions

Declaration Summary
fun FLine.isoWeights(key: String = FLINE_ISO_WEIGHT_KEY): FLinePointWeight Read a per-node weight property off this line's instructions and interpolate it into an FLinePointWeight, for generateIsoContours and friends.
fun pointEdgeScalarField2D(points: List<Vec2>, edges: List<Pair<Int, Int>>, pointRadius: Double, edgeRadius: Double, pointWeight: PointEdgeWeight? = null): ScalarField2D Build a live scalar field from a graph of 2D points plus edges between them.
fun pointEdgeScalarField2D(points: List<Vec2>, edges: List<Pair<Int, Int>>, radius: Double, pointWeight: PointEdgeWeight? = null): ScalarField2D
fun pointEdgeScalarField2D(line: FLine, pointRadius: Double, edgeRadius: Double, samplesPerCubic: Int = 24, pointWeight: FLinePointWeight? = null): ScalarField2D Flatten a 2D FLine into a point/edge graph and build the union-of-discs-and-capsules field around it. See FLinePointWeight and FLine.isoWeights for the taper.
fun pointEdgeScalarField2D(line: FLine, radius: Double, samplesPerCubic: Int = 24, pointWeight: FLinePointWeight? = null): ScalarField2D
fun generateIsoContours(field: ScalarField2D, xMin: Double, xMax: Double, yMin: Double, yMax: Double, nx: Int, ny: Int, iso: Double): List<List<Vec2>> Generate iso-contours for a scalar field using marching squares.
fun generateIsoContoursWithStep(field: ScalarField2D, xMin: Double, xMax: Double, yMin: Double, yMax: Double, spatialStep: Double, value: Double): List<List<Vec2>> Convenience overload taking approximate spatial resolution instead of nx, ny.
fun generateIsoContours(line: FLine, pointRadius: Double, edgeRadius: Double, spatialStep: Double, iso: Double = 0.0, padding: Double? = null, samplesPerCubic: Int = 24, pointWeight: FLinePointWeight? = null): List<List<Vec2>> Convenience overload that turns a 2D FLine into a point/edge scalar field, expands its bounds by the effective tube radius, and extracts contours.
fun generateIsoContours(line: FLine, radius: Double, spatialStep: Double, iso: Double = 0.0, padding: Double? = null, samplesPerCubic: Int = 24, pointWeight: FLinePointWeight? = null): List<List<Vec2>>
fun smoothIsoContours(contours: List<List<Vec2>>, xMin: Double, xMax: Double, yMin: Double, yMax: Double, tension: Float = 1f, boundaryEpsilon: Double = 1e-6): List<FLine> Smooth marching-squares polylines into cubic curves.
fun generateSmoothIsoContours(line: FLine, pointRadius: Double, edgeRadius: Double, spatialStep: Double, iso: Double = 0.0, padding: Double? = null, samplesPerCubic: Int = 24, pointWeight: FLinePointWeight? = null, tension: Float = 1f, boundaryEpsilon: Double = spatialStep * 0.75): List<FLine> The whole FLine in, smooth outline out path. See FLinePointWeight and FLine.isoWeights to taper it.
fun generateSmoothIsoContours(line: FLine, radius: Double, spatialStep: Double, iso: Double = 0.0, padding: Double? = null, samplesPerCubic: Int = 24, pointWeight: FLinePointWeight? = null, tension: Float = 1f, boundaryEpsilon: Double = spatialStep * 0.75): List<FLine>
fun pointEdgeGraph2D(line: FLine, samplesPerCubic: Int = 24): PointEdgeGraph2D Flatten a 2D FLine into the points and edges that pointEdgeScalarField2D turns into a field: one point per moveTo/lineTo, samplesPerCubic per cubicTo, and an edge for every pair of consecutive points — including the wrap-around edge of a close.
fun isoContourClosed(contour: List<Vec2>, xMin: Double, xMax: Double, yMin: Double, yMax: Double, boundaryEpsilon: Double = 1e-6): Boolean Does this marching-squares contour close on itself, or does it run out of the sampling window?

Top-level values

Declaration Summary
const val FLINE_ISO_WEIGHT_KEY Default property name that FLine.isoWeights reads a per-node weight from.
var FLine.Node.isoWeight: Double? Per-node tube weight — a multiplier on pointRadius at this instruction, which FLine.isoWeights interpolates into an FLinePointWeight. null, the default, means "this node is not a keyframe".
var FLine.isoWeight: Double? Line-wide fallback weight, used for every node when no node sets an isoWeight of its own.

PointEdgeGraph2D

data class PointEdgeGraph2D(val points: List<Vec2>, val edges: List<Pair<Int, Int>>, val nodeIndex: List<Double>)

A 2D FLine flattened into the point/edge graph that pointEdgeScalarField2D contours, plus the provenance of each point.

Declaration Summary
val points: List<Vec2>
val edges: List<Pair<Int, Int>>
val nodeIndex: List<Double>

PointEdgeWeight

typealias PointEdgeWeight = (index: Int, point: Vec2) -> Double

Per-point weight for a field built from an explicit point/edge graph — a multiplier on pointRadius for the disc around pointsindex``.

FLinePointWeight

typealias FLinePointWeight = (nodeIndex: Double, point: Vec2) -> Double

Per-point weight for a field built from an FLine — a multiplier on pointRadius for the disc around one flattened sample of the line.

ScalarField2D

typealias ScalarField2D = (Double, Double) -> Double

Scalar field f(x, y) -> value. You can use a lambda or method reference, e.g. rbf::interpolate2D.

FLinesAndJavaShapes.kt

Converts FLines to and from java.awt.geom shapes, and walks them by distance with FLinesAndJavaShapes.Cursor.

The round trip through java.awt.geom (geometry only, not attributes) gives 2D FLines Java2D's flattening, stroking, insetting and outsetting. Cursor moves along a line by arc length or by node parameter, reports the position, tangent and curvature there, and splits the line. CubicSegment2 and CubicSegment3 hold the per-segment cubic maths: roots, inflections, bounding boxes and intersections.

src/jvmMain/kotlin/afield/geometry/path/java2d/FLinesAndJavaShapes.kt — read the source

FLinesAndJavaShapes

class FLinesAndJavaShapes

Utility classes for manipulating and traversing FLines.

Declaration Summary
fun fromFourPoints(p0: Vec3, u: Float, p1: Vec3, v: Float, p2: Vec3, p3: Vec3): Pair<Vec3, Vec3>? returns the c1, c2 control points for a cubic segement to go through p0@0, p1@u, p2@v and p3@ t=1.0
fun segment(f: FLine): List<FLine> segments this along .moveTo's
fun positions(f: FLine, flatness: Float): List<Vec3> A list of low curvature positions out of a FLine
class Cursor(on: FLine, private val tol: Float) A class for navigating FLines either by distance "D" or by node "T".
class CubicSegment3
inner class CubicSegment2 helper class for a 2d cubic segment, for the things that we can only reasonably do in 2d (like intersections).

ManifoldHeaderCheck.kt

Checks every FFM descriptor ManifoldNative binds against the prototypes in the installed manifoldc.h, and against the structs and enums in its types.h. A descriptor has to declare the same number of arguments, and at every position, and for the return, the same kind of value: pointer, double, float, 32- or 64-bit integer (size_t is 64), void, or a struct passed by value of the same size.

A descriptor that disagrees with the header still links and still returns a number, so this is the only thing that notices one. ManifoldSmokeTest runs it first.

Pointer types are not compared, since FFM has one ADDRESS. The two places where the pointee matters are handled once in the wrapper rather than at each call: Solid.slice and Solid.project convert the ManifoldPolygons that manifold_slice and manifold_project return into a Section in one place, and Solid.raycast turns the C API's segment fraction into a distance in the solid's units.

src/jvmMain/kotlin/afield/geometry/solid/manifold/ManifoldHeaderCheck.kt — read the source

ManifoldHeaderCheck

object ManifoldHeaderCheck

Every bound FFM descriptor against the installed manifoldc.h; check returns what disagrees.

Declaration Summary
data class Prototype(val name: String, val returns: String, val arguments: List<String>) One C prototype: its return type and argument types, as written.
fun headerCandidates(): List<File> Where manifoldc.h is looked for; AFIELD_MANIFOLD_HEADER names it directly.
fun header(): File? The first of headerCandidates that exists, or null when none of them does.
fun check(header: File): List<String> Every problem with the bound descriptors, or empty. Binds every handle ManifoldNative declares first.

ManifoldSmokeTest.kt

src/jvmMain/kotlin/afield/geometry/solid/manifold/ManifoldSmokeTest.kt — read the source

ManifoldSmokeTest

object ManifoldSmokeTest

The Manifold binding, end to end, against geometry whose answers are known in closed form.

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

PlanarContact.kt

Finds where two parts touch on a shared plane, and audits apertures cut through that contact, in double precision.

planarContact intersects the boundary triangles of two SolidPlanarMeshes lying on a ContactPlane and returns a ContactPatch; planarContacts discovers the shared planes itself. auditApertures compares removed regions against an expected boundary and reports mismatch areas against GeometryTolerances. polylineTube builds a Solid tube of cylinders and spheres along a polyline.

src/jvmMain/kotlin/afield/geometry/solid/manifold/PlanarContact.kt — read the source

Used in: Solids that cannot fail: Manifold in Field

Top-level functions

Declaration Summary
fun planarContact(a: SolidPlanarMesh, b: SolidPlanarMesh, plane: ContactPlane, tolerances: GeometryTolerances = GeometryTolerances()): ContactPatch
fun auditApertures(stockA: SolidPlanarMesh, stockB: SolidPlanarMesh, removedA: SolidPlanarMesh, removedB: SolidPlanarMesh, expectedBoundary: SolidPlanarMesh, plane: ContactPlane, tolerances: GeometryTolerances = GeometryTolerances(), minimumContactMargin: Double = 0.0): ApertureAudit Compare actual removed boundaries against an explicitly supplied expected aperture boundary on the same plane.
fun planarContacts(a: SolidPlanarMesh, b: SolidPlanarMesh, tolerances: GeometryTolerances = GeometryTolerances()): List<ContactPatch> Discover boundary planes shared by two parts. Coincident triangles in remote parts produce no patch.
fun polylineTube(points: List<Point3D>, radius: Double, radialSegments: Int = 48): Solid Union of polygonal cylinders and spheres along a saved polyline; not a toolpath or exact circular sweep.

ContactPlane

data class ContactPlane(val origin: Point3D, val normal: Point3D)

A finite physical contact is extracted from boundary triangles on this plane, not from an infinite cut.

ContactPatch

data class ContactPatch(val plane: ContactPlane, val u: Point3D, val v: Point3D, val rings: List<List<Pair<Double,Double>>>, val area: Double)

Declaration Summary
val rings: List<List<Pair<Double,Double>>>, val area: Double)

ApertureAudit

data class ApertureAudit(val contact: ContactPatch, val apertureA: ContactPatch, val apertureB: ContactPatch, val expected: ContactPatch, val mismatchArea: Double, val expectedMismatchArea: Double, val tolerances: GeometryTolerances, val outsideContactArea: Double = 0.0, val insufficientMarginArea: Double = 0.0, val requiredMargin: Double = 0.0)

Declaration Summary
val expected: ContactPatch, val mismatchArea: Double, val expectedMismatchArea: Double
val tolerances: GeometryTolerances, val outsideContactArea: Double
val insufficientMarginArea: Double
val passed: Boolean

GeometryTolerances

data class GeometryTolerances(val length: Double = 1e-6, val area: Double = 1e-4, val volume: Double = 1e-3)

Length, area and volume thresholds in caller-chosen units.

Section.kt

2D regions with holes, from Manifold's CrossSection: boolean operations, offsetting with a choice of JoinType, and extrusion into a Solid.

FLine.toSection converts drawn outlines into a Section, and Section.toFLines converts back for display.

src/jvmMain/kotlin/afield/geometry/solid/manifold/Section.kt — read the source

Top-level functions

Declaration Summary
fun FLine.toSection(sampleDistance: Double = 1.0, fillRule: FillRule = FillRule.POSITIVE): Section Section.ofFLine, as an extension, for reading left to right off an existing line.
fun List<FLine>.toSection(sampleDistance: Double = 1.0, fillRule: FillRule = FillRule.POSITIVE): Section Section.ofFLines, as an extension.
fun Section.toVec3s(z: Double = 0.0): List<Vec3> The 3D points of a Section's contours, on the z = 0 plane — for Solid.hullOf and friends.

Section

class Section internal constructor(handle: MemorySegment) : AutoCloseable

Section — a 2D region with holes, that you can offset. Manifold's CrossSection, which is Clipper2 underneath.

Declaration Summary
val isClosed: Boolean
operator fun plus(other: Section): Section
operator fun minus(other: Section): Section
infix fun and(other: Section): Section
infix fun or(other: Section): Section
fun offset(delta: Double, joinType: JoinType = JoinType.ROUND, miterLimit: Double = 2.0, circularSegments: Int = 0): Section Grow (or shrink, for a negative delta) the region by delta, as a true parallel curve.
fun translated(x: Double, y: Double): Section
fun rotated(degrees: Double): Section Rotate about the origin, in degrees.
fun scaled(x: Double, y: Double): Section
fun scaled(factor: Double): Section
fun mirrored(normalX: Double, normalY: Double): Section Mirror through the line through the origin with this normal.
fun simplified(epsilon: Double): Section Drop vertices that are within epsilon of the line between their neighbours — the Douglas-Peucker-ish cleanup for a contour that came out of a Solid.slice or a dense offset.
fun hull(): Section The convex hull of the region.
fun decompose(): List<Section> Split into separate regions, largest first by area. Holes stay with the outline that owns them.
fun extruded(height: Double, slices: Int = 0, twistDegrees: Double = 0.0, scaleTop: Double = 1.0, scaleTopY: Double = scaleTop): Solid Extrude along +Z, optionally twisting and tapering on the way — a lathe's opposite number.
fun revolved(segments: Int = 0, revolveDegrees: Double = 360.0): Solid A lathe: the cross-section's x becomes a radius and its y becomes the solid's z, so the resulting solid's axis of symmetry is Z.
val area: Double Enclosed area, holes subtracted.
val vertexCount: Long
val contourCount: Long Contours, outlines and holes together. An annulus is 2.
val isEmpty: Boolean
val bounds: Pair<Vec2, Vec2> The bounding rectangle, as (minX, minY) to (maxX, maxY).
fun toPolygons(): List<List<Vec2>> The contours, as point lists. Outlines are counter-clockwise, holes clockwise — see the winding note on Section.
fun toFLines(z: Double = 0.0): List<FLine> The contours as closed FLines, ready to display(), thicken, animate or export — the way back into everything Field already does with a line.
fun toFLine(z: Double = 0.0): FLine Every contour merged into one FLine with a sub-path each — the single-value form of toFLines.
fun copy(): Section An independent copy — the one thing that survives its enclosing Solid.scope.
override fun close()
override fun toString(): String

Solid.kt

Closed 3D volumes from the Manifold library: primitives, transforms, union, difference and intersection, with results that stay closed manifolds.

Solid is the type a box uses; TriMesh.toSolid brings an existing mesh in, and Solid.toTriMesh hands the result to Field's mesh display. The native library is optional: ManifoldLibrary reports whether it is installed.

src/jvmMain/kotlin/afield/geometry/solid/manifold/Solid.kt — read the source

Used in: Solids that cannot fail: Manifold in Field

Top-level functions

Declaration Summary
fun TriMesh.toSolid(check: Boolean = true, weldTolerance: Double? = null): Solid Read a TriMesh into Manifold, so it can be cut.
fun TriMesh.isValidSolid(): Boolean true when Manifold agrees this mesh is a closed solid and it is not inside out (a negative total volume) — the cheap pre-flight for toSolid, and false exactly when toSolid() would throw.

Solid

class Solid internal constructor(handle: MemorySegment) : AutoCloseable, ToHTML

Solid — a guaranteed-closed 3D volume you can subtract, union and intersect without thinking about whether the answer is still a surface. Field's binding to Manifold, through Panama/FFM.

Declaration Summary
fun <T> sampleSlices(n: Int, fillRule: FillRule = FillRule.POSITIVE, sample: (z: Double, section: Section) -> T): List<T> Sample n evenly spaced horizontal layer midpoints, from the bottom bound upward. Each Section is closed after sample returns, including when it throws. Return copied values such as area or polygons; a native section retained from the callback will be closed. fillRule has the same meaning as in slice. Integration weights remain the caller's choice.
val isClosed: Boolean true once close (or an enclosing scope) has released the native volume.
operator fun plus(other: Solid): Solid Union — the volume in either solid.
operator fun minus(other: Solid): Solid Difference — this solid with other removed.
infix fun and(other: Solid): Solid Intersection — only the volume in both solids.
infix fun or(other: Solid): Solid Alias for plus, for the reading where a union is an or.
fun minkowskiSum(other: Solid): Solid The Minkowski sum — every point of this solid displaced by every point of other. Sweeping a small sphere over a part is how you round all of its edges at once:
fun minkowskiDifference(other: Solid): Solid The Minkowski difference — minkowskiSum's erosion counterpart, for shrinking by a shape.
fun splitByPlane(normalX: Double, normalY: Double, normalZ: Double, offset: Double = 0.0): Pair<Solid, Solid> Cut by a plane and keep both halves: first is the side the normal points toward, second is the rest.
fun split(other: Solid): Pair<Solid, Solid> Split by another solid: first is the part inside other, second the part outside.
fun trimByPlane(normalX: Double, normalY: Double, normalZ: Double, offset: Double = 0.0): Solid Keep only the half the normal points toward — splitByPlane's first, without building the offcut. The cheap way to open a solid up and look inside it.
fun decompose(): List<Solid> Split into connected components, largest first by volume.
fun translated(x: Double, y: Double, z: Double): Solid
fun translated(v: Vec3): Solid
fun rotated(xDegrees: Double, yDegrees: Double, zDegrees: Double): Solid Rotate by Euler angles in degrees, applied X then Y then Z — Manifold's own convention.
fun scaled(x: Double, y: Double, z: Double): Solid
fun scaled(factor: Double): Solid
fun mirrored(normalX: Double, normalY: Double, normalZ: Double): Solid Mirror through the plane with this normal, through the origin.
fun transformed(c0x: Double, c0y: Double, c0z: Double, c1x: Double, c1y: Double, c1z: Double, c2x: Double, c2y: Double, c2z: Double, tx: Double, ty: Double, tz: Double): Solid Apply a 3x4 affine transform, given column-major as three basis columns and a translation — the same 12 numbers Manifold takes.
fun warped(map: (Double, Double, Double) -> Triple<Double, Double, Double>): Solid Move every vertex by a Kotlin function — a bend, a twist, a taper — without re-meshing.
fun refined(n: Int): Solid Subdivide every triangle into n² smaller ones, so a later warped or smoothOut has somewhere to act. Triangle count grows as the square of n.
fun refinedToLength(length: Double): Solid Subdivide until no edge is longer than length — the resolution-independent refined.
fun refinedToTolerance(tolerance: Double): Solid Subdivide until the mesh is within tolerance of its own smooth interpolation.
fun smoothOut(minSharpAngle: Double = 60.0, minSmoothness: Double = 0.0): Solid Round the mesh off, treating edges sharper than minSharpAngle degrees as creases to keep.
fun simplified(tolerance: Double): Solid Collapse edges shorter than tolerance, keeping the solid valid.
fun withTolerance(tolerance: Double): Solid Set the ε Manifold treats as "the same point" for later operations.
fun hull(): Solid The convex hull of this solid.
fun slice(height: Double = 0.0, fillRule: FillRule = FillRule.POSITIVE): Section The cross section at z = height — a real slice, not a silhouette.
fun project(fillRule: FillRule = FillRule.POSITIVE): Section The shadow of the whole solid on the XY plane — the union of every slice.
val volume: Double Enclosed volume. Exact for the primitives — Solid.cube(2,3,4).volume is 24.0, not 23.999997 — because Manifold measures the solid rather than summing float triangles.
val surfaceArea: Double Total surface area. Exact for the primitives, for the same reason as volume.
val vertexCount: Long
val edgeCount: Long
val triangleCount: Long
val propertyCount: Long Per-vertex property channels beyond position, if any were carried through.
val genus: Int Topological genus — the number of holes through the solid. A block is 0, a torus 1, a plate with three bores 3.
val isEmpty: Boolean true for a solid with no volume — including the result of a failed operation.
val status: ManifoldStatus Why the last operation could not produce a valid solid, or ManifoldStatus.NO_ERROR.
val tolerance: Double The ε this solid treats as "the same point".
val epsilon: Double Manifold's own precision estimate for this solid's coordinate range.
val bounds: AxisAlignedBoundingBox3D The axis-aligned bounds, as the same type Field's mesh family and afield.geometry.mesh.marchingCubes use.
fun minGap(other: Solid, searchLength: Double): Double The smallest gap between this solid's surface and other's, searching out to searchLength.
fun raycast(originX: Double, originY: Double, originZ: Double, dirX: Double, dirY: Double, dirZ: Double, length: Double = 1e4): List<RayHit> Every intersection of a ray with this solid's surface, near to far.
fun contains(x: Double, y: Double, z: Double): Boolean true when x, y, z is inside the solid, by raycast's parity test.
fun checked(what: String = "solid"): Solid Throw if this solid is not valid, naming the status. Put it at the end of a boolean chain, where an invalid result would otherwise travel on as a 0.0 volume.
fun toPlanarMesh(): SolidPlanarMesh Export the native polygon-face topology without narrowing positions to floats.
fun toTriMesh(normals: Boolean = false, minSharpAngle: Double = 60.0): TriMesh The TriMesh Field's mesh family, visualizers, FLine bridge and OBJ/STL writers all take.
override fun toHTML(): String The inline card: toTriMesh's, so a Solid draws like every other mesh in Field, plus the numbers only a solid has.
override fun toString(): String
override fun close() Release the native volume now.
fun copy(): Solid An independent copy — the one thing that survives its enclosing scope.

SolidPlanarMesh.kt

A double-precision export of a Solid's boundary that keeps Manifold's planar-face provenance.

Solid.toPlanarMesh produces a SolidPlanarMesh; each SolidPlanarFace groups the triangles of one planar face. SolidPlanarMesh.toSolid rebuilds a Solid from it, and SolidPlanarMesh.toTriMesh is the lossy float conversion for rendering.

src/jvmMain/kotlin/afield/geometry/solid/manifold/SolidPlanarMesh.kt — read the source

Top-level functions

Declaration Summary
fun SolidPlanarMesh.toSolid(check: Boolean = true): Solid Rebuild a native solid without the Float32 narrowing of TriMesh.toSolid.

SolidPlanarFace

data class SolidPlanarFace(val runIndex: Int, val originalId: Int?, val faceId: Long, val triangles: IntArray)

One polygonal face in Manifold's full-precision mesh export.

Declaration Summary
val runIndex: Int
val originalId: Int?
val faceId: Long
val triangles: IntArray Triangle ordinals in SolidPlanarMesh.indices, after degenerate index triangles are removed.

SolidPlanarMesh

data class SolidPlanarMesh(val positions: DoubleArray, val indices: IntArray, val triangleFaces: IntArray, val faces: List<SolidPlanarFace>, val nativeTolerance: Double)

Manifold's full-precision boundary mesh: double-precision positions plus native polygonal-face provenance.

Declaration Summary
val positions: DoubleArray
val indices: IntArray
val triangleFaces: IntArray
val faces: List<SolidPlanarFace>
val nativeTolerance: Double
val vertexCount: Int
val triangleCount: Int
fun toTriMesh(): TriMesh

SolidCraft.kt

An optional bridge to the SolidCraft B-rep document engine, whose model stays in native Rust code.

SolidCraftLibrary locates the native library and reports whether it is available. SolidCraftDesign.open loads a design, SolidCraftDesign.setParameter edits it, and SolidCraftDesign.mesh copies a body out as a SolidCraftMesh for display.

src/jvmMain/kotlin/afield/geometry/solid/solidcraft/SolidCraft.kt — read the source

Used in: SolidCraft: edit the design, then see the part

SolidCraftLibrary

object SolidCraftLibrary

Optional SolidCraft B-rep/document engine. Its model remains in Rust; Field copies display meshes.

Declaration Summary
val searchedPaths: List<Path>
val resolvedPath: Path?
val isAvailable: Boolean
fun describe(): String

SolidCraftMesh

data class SolidCraftMesh(val positions: DoubleArray, val indices: IntArray, val faceIds: IntArray)

Double-precision SolidCraft tessellation plus B-rep face IDs, copied out of Rust memory.

Declaration Summary
fun toTriMesh(): TriMesh

SolidCraftDesign

class SolidCraftDesign private constructor(private var handle: MemorySegment) : AutoCloseable

A saved SolidCraft design with an incremental evaluator. Close it when finished.

Declaration Summary
fun summary(): JSONObject
fun setParameter(name: String, expression: String) An invalid expression or failed feature leaves the previous design and mesh intact.
fun mesh(bodyIndex: Int = 0, tolerance: Double = 0.05): SolidCraftMesh
fun exportStep(): ByteArray
fun exportStl(): ByteArray
fun saveDesign(): ByteArray
override fun close()

SolidCraftBuilder.kt

Writing a SolidCraft part in Kotlin instead of drawing it: sketches, the features that turn them into solids and surfaces, and named parameters that stay editable afterwards.

solidCraft opens a SolidCraftBuilder, whose calls accumulate into SolidCraft's own design document, the same JSON a saved design holds. So a part written here is evaluated by the engine that opens one from disk, and everything downstream treats the two alike. SolidCraftBuilder.json is that document and SolidCraftBuilder.open evaluates it, giving back a SolidCraftDesign to mesh, re-parameterise or export.

Only the evaluation needs the native bridge, since the document is assembled as plain JSON. A box can therefore build and inspect a design on a machine where SolidCraft was never compiled, and SolidCraftLibrary says whether this one has it.

Lengths and angles are SolidCraft expressions rather than numbers: "12" in the design's units, a quantity such as "30 deg", or a formula over the design's parameters such as "width/2". That is what keeps a finished part adjustable, through SolidCraftDesign.setParameter, without rebuilding it here.

src/jvmMain/kotlin/afield/geometry/solid/solidcraft/SolidCraftBuilder.kt — read the source

Used in: SolidCraft: edit the design, then see the part

Top-level functions

Declaration Summary
fun solidCraft(name: String = "Untitled", units: String = "mm", build: SolidCraftBuilder.() -> Unit): SolidCraftBuilder Builds a SolidCraft design from Kotlin: sketches of points and curves (lines, arcs, circles, splines, 3D wires) and the features that turn them into bodies and surfaces — extrude, revolve, sweep with guide rails, loft, pipe, and the surface sweep, loft, revolve and boundary patch.

SolidCraftSketchRef

class SolidCraftSketchRef internal constructor(val id: Long, val name: String, val curves: List<String>, val wires: List<String>)

A sketch feature already added to a SolidCraftBuilder; pass it to the features that use it.

SolidCraftFeatureRef

class SolidCraftFeatureRef internal constructor(val id: Long, val name: String)

A feature added to a SolidCraftBuilder, by the id and name SolidCraft reports it under.

SolidCraftRailFit

enum class SolidCraftRailFit(internal val json: String)

How a sweep's guide rails reshape its profile: evenly (SCALE), or only across to the rail.

Constants: SCALE, STRETCH.

SolidCraftOperation

enum class SolidCraftOperation(internal val json: String)

How a joining feature meets bodies already in the design.

Constants: NEW_BODY, JOIN, CUT, INTERSECT.

SolidCraftContinuity

enum class SolidCraftContinuity(internal val json: String)

How much of a neighbouring surface a patch or loft end repeats: position, tangent, curvature, its change.

Constants: G0, G1, G2, G3.

SolidCraftBuilder

class SolidCraftBuilder internal constructor(private val name: String, private val units: String)

Declaration Summary
fun param(name: String, expression: String, unit: String = inferUnit(expression)) A named, editable parameter: param("width", "40 mm"). unit is inferred from a plain quantity.
fun sketch(plane: Any = "XY", draw: SolidCraftSketch.() -> Unit): SolidCraftSketchRef Adds a sketch on plane ("XY", "XZ" or "YZ", or offsetPlane), drawn by draw. Coordinates are the sketch's own 2D ones; wires are 3D, in world coordinates.
fun offsetPlane(base: String, distance: String): JSONObject base ("XY"…) moved along its normal by distance, for stacking loft sections.
fun planeSquareTo(line: FLine): JSONObject The plane through the start of line, square to it there, with the sketch origin on the line. A profile drawn about (0, 0) on it sweeps along wire(line) as a true tube, because a sweep keeps the angle the profile was drawn at to the path's start.
fun extrude(sketch: SolidCraftSketchRef, distance: String, operation: SolidCraftOperation = SolidCraftOperation.NEW_BODY, symmetric: Boolean = false, taper: String? = null) Extrudes every closed profile of sketch by distance along its normal.
fun revolve(sketch: SolidCraftSketchRef, axis: String = "y", angle: String = "360 deg", operation: SolidCraftOperation = SolidCraftOperation.NEW_BODY) Revolves every closed profile of sketch about its sketch axis ("x" or "y").
fun sweep(profile: SolidCraftSketchRef, path: SolidCraftSketchRef, pathCurves: List<String> = path.chain(), rails: List<SolidCraftSketchRef> = emptyList(), railFit: SolidCraftRailFit = SolidCraftRailFit.SCALE, twist: String? = null, scale: String? = null, operation: SolidCraftOperation = SolidCraftOperation.NEW_BODY) Sweeps the closed profiles of profile along path (its curves and wires, as one chain unless pathCurves picks some). Each of rails is a sketch whose chain the profile's far side follows, reshaping it by railFit. Without rails, twist turns the profile along the way and scale grows it to the end; with rails, the rails decide both.
fun loft(sections: List<SolidCraftSketchRef>, operation: SolidCraftOperation = SolidCraftOperation.NEW_BODY) A solid through the closed profiles of sections, in order (each sketch one section).
fun pipe(path: SolidCraftSketchRef, diameter: String, wall: String? = null, pathCurves: List<String> = path.chain(), operation: SolidCraftOperation = SolidCraftOperation.NEW_BODY) A round tube of diameter along path; wall makes it hollow.
fun fillet(radius: String, vararg edgesNear: DoubleArray) A fillet of radius on the edges nearest each of edgesNear (world points).
fun surfaceSweep(profile: SolidCraftSketchRef, path: SolidCraftSketchRef, profileCurves: List<String> = profile.curves, pathCurves: List<String> = path.chain(), rails: List<SolidCraftSketchRef> = emptyList(), railFit: SolidCraftRailFit = SolidCraftRailFit.SCALE, twist: String? = null, scale: String? = null) A surface (open sheet body) swept from the curves of profile along path; rails as in sweep.
fun surfaceLoft(sections: List<SolidCraftSketchRef>, start: SolidCraftContinuity = SolidCraftContinuity.G0, end: SolidCraftContinuity = SolidCraftContinuity.G0) A surface through one curve chain per section sketch; start/end set how it leaves the first and last.
fun surfaceRevolve(profile: SolidCraftSketchRef, axis: String = "y", angle: String = "360 deg", curves: List<String> = profile.curves) A surface of revolution of the curves of profile about its sketch axis.
fun surfaceBoundary(sketch: SolidCraftSketchRef, curves: List<String> = sketch.curves, orders: List<SolidCraftContinuity> = emptyList(), crown: Double = 0.0, samples: Int = 16) One patch through the boundary curves of sketch, given in loop order (each ends where the next begins). Two curves give the bridge between them; three or four give the patch their sides span, which takes no crown. Draw both curves in the same direction: the bridge joins start to start, so opposed curves give a patch folded over itself. orders is the continuity across each boundary; crown fills the middle (0 plain, up to 3 fuller, down to -0.9 flatter).
fun thicken(bodies: List<String>, thickness: String, symmetric: Boolean = false) Thickens surface bodies (by name, as SolidCraftDesign.summary lists them) into solids.
fun feature(type: String, fields: JSONObject) Any other SolidCraft feature, as its JSON fields beside "type" (snake_case, as in FeatureKind): feature("shell", JSONObject().put("faces", ...).put("thickness", "2")).
fun json(): JSONObject The design as SolidCraft's JSON document.
fun open(): SolidCraftDesign Evaluates the design; a feature that fails throws with SolidCraft's message.

SolidCraftSketch

class SolidCraftSketch internal constructor()

The points and curves of one sketch. Each drawing call returns the new curve's id, which features take to pick curves (pathCurves, surfaceBoundary(curves = ...)). Endpoints that coincide are shared, so consecutive lines and splines form one connected chain.

Declaration Summary
fun line(x0: Double, y0: Double, x1: Double, y1: Double): String
fun circle(cx: Double, cy: Double, r: Double): String
fun arc(cx: Double, cy: Double, x0: Double, y0: Double, x1: Double, y1: Double): String An arc about (cx, cy) running counter-clockwise from (x0, y0) to (x1, y1).
fun spline(vararg xy: Double, periodic: Boolean = false, degree: Int = 3): String A smooth spline through xy (x0, y0, x1, y1, …); periodic closes it.
fun rectangle(x0: Double, y0: Double, x1: Double, y1: Double): List<String> Four lines around the rectangle (x0, y0)–(x1, y1).
fun polygon(vararg xy: Double): List<String> A closed polyline through (x, y) pairs.
fun wire(vararg xyz: Double): String A 3D curve in world coordinates through xyz (x0, y0, z0, …), usable as a sweep path, rail or loft section.
fun wire(line: FLine, spacing: Float = 1f): String line as a 3D wire, sampled every spacing along its length (its own coordinates, z included).

SolidCraftBuilderSmokeTest.kt

src/jvmMain/kotlin/afield/geometry/solid/solidcraft/SolidCraftBuilderSmokeTest.kt — read the source

SolidCraftBuilderSmokeTest

object SolidCraftBuilderSmokeTest

Run with ./gradlew smokeSolidCraft: builds one design per construction feature and checks its geometry.

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

SolidCraftSmokeTest.kt

src/jvmMain/kotlin/afield/geometry/solid/solidcraft/SolidCraftSmokeTest.kt — read the source

SolidCraftSmokeTest

object SolidCraftSmokeTest

Run with ./gradlew smokeSolidCraft after opting into the native build.

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

bspot.kt

One-to-one matching between two equally sized 2D point clouds, at a million points a side, using BSP-OT: every source point gets exactly one target and every target is used exactly once, with the total squared travel distance kept small.

The exact answer to that problem is an assignment problem, and the Hungarian method that assignment in transport.kt solves it with is O(n³) time over an n × n cost matrix. At a thousand points that is fine. At a million the matrix alone is four terabytes. BSP-OT gives up provable optimality to get to O(n log n):

  1. Choose a direction. Split each cloud at its own median along it, so both left halves hold the same number of points, and so do both right halves.
  2. Recurse, left with left and right with right, with a new direction each time. A leaf of one point pairs its source with its target; a leaf of two picks the cheaper of the two pairings.
  3. That is one candidate bijection. Make several and merge each new one into the best so far: the union of two permutations falls apart into disjoint alternating cycles, and on each cycle you keep whichever orientation is cheaper. Then try two-point swaps along the candidate's edges.

The direction in step 1 is what BspOt2D.Splits chooses. The default, as in the reference implementation, fits a Gaussian to each of the two subsets and splits along an eigenvector of the linear map that carries one Gaussian onto the other, picking one of the two at random per node. When the subsets differ in shape that axis is the one along which they need to move, and it about halves the excess cost of random directions at eight trees. Below 50 points it stops fitting and splits perpendicular to the parent.

The counts are equal by construction, not because the two clouds occupy the same cells, so clouds of very different density still match completely.

val result = matchPoints(from, to)            // two List<Vec2> of the same size
val destination = result.reorder(to)          // destination[i] is where from[i] goes
val halfway = from.indices.map { from[it] + (destination[it] - from[it]) * 0.5f }

For raw FloatArrays, call BspOt2D.match directly. A previous permutation can be passed as initial and is improved rather than replaced, which suits matching successive frames of a simulation.

Cost, measured

On an M1 Max with the default four workers and eight trees, a million points a side takes 3 to 4.5 s for two uniform clouds, and 5.7 s from a uniform square onto three discs (7.4 s with random splits, which reach a 5% higher cost). Merging is sequential. Array storage is about 69 + 60 P bytes a point for P workers, roughly 309 MB at a million points and four workers. The SIMD paths (projection, and cost evaluation through a gather) gave no measurable end-to-end gain over simd = false when this was first measured; partitioning, cycle walking and the swaps are scalar.

What it does not give you

  • No optimality guarantee, and further from optimal than the paper. From a uniform square onto three discs, 1000 points, the total cost is 5.5% above the exact Hungarian answer with 8 trees and 2.9% with 32. Two independent uniform clouds are the hard case: every node's two subsets have nearly the same Gaussian, so the transport map is close to the identity and its eigenvectors are noise. At 400 points that lands 26% above exact with 64 trees, where random splits reach 16%. Leaving those clouds unmatched is 120 times worse. The paper reports typically under 1% with 64 candidates; it ranks all candidates before merging, which this version, merging as it goes in bounded memory, does not. The bijection is good enough for a morph to read as a morph; it is not a solver for the exact assignment.
  • 2D only, equal sizes only, uniform weights only. No partial or unbalanced transport.
  • Deterministic for a given seed and options, across worker counts. Not promised bit-identical across JDKs or CPUs.

Algorithm from Baptiste Genest, Nicolas Bonneel, Vincent Nivoliers and David Coeurjolly, "BSP-OT: Sparse transport plans between discrete measures in loglinear time", ACM Transactions on Graphics (SIGGRAPH Asia 2025), https://perso.liris.cnrs.fr/david.coeurjolly/publication/bspot-25/. The Gaussian split follows gaussianPartialBSPOT and the merge MergePlansNoPar and BijectiveMatching::swapIfUpgrade in their MIT-licensed reference implementation, https://github.com/baptiste-genest/BSP-OT, Copyright (c) 2025 the same authors.

src/jvmMain/kotlin/afield/geometry/spatial/bspot.kt — read the source

Used in: A Drawn Morph, Heard

Top-level functions

Declaration Summary
fun List<Vec2>.resampleByLength(count: Int, closed: Boolean = false): List<Vec2> Exactly count points spaced evenly along this polyline by arc length, first and last point included — the step that turns two strokes with different numbers of unevenly spaced points (a hand slows into a corner) into two lists matchPoints can pair. With closed, the polyline returns to its first point and the samples go round the loop without repeating it.
fun matchPoints(from: List<Vec2>, to: List<Vec2>, trees: Int = 16, seed: Long = 1L, initial: IntArray? = null): BspOt2D.Result Matches from onto to one-to-one with BspOt2D; both lists must be the same size.

BspOt2D

object BspOt2D

Equal-cardinality, equal-mass 2D BSP-OT with squared Euclidean cost: median BSP proposals split along Gaussian-guided or random directions, merged by alternating cycles and improving two-point swaps.

Declaration Summary
val simdLanes: Int Doubles per SIMD register on this machine.
enum class Splits How each BSP node chooses the direction it splits both subsets along.
const val GAUSSIAN_LEAF Subsets smaller than this stop fitting Gaussians under Splits.GAUSSIAN.
data class Options(val trees: Int = 16, val splits: Splits = Splits.GAUSSIAN, val seed: Long = 1L, val simd: Boolean = true, val cycleMerge: Boolean = true, val swapPasses: Int = 1, val parallelism: Int = minOf(4, Runtime.getRuntime().availableProcessors())) Settings for match.
class Result(val targetOfSource: IntArray, val squaredCost: Double, val costHistory: DoubleArray) A bijection from sources to targets, and what it cost.
fun match(ax: FloatArray, ay: FloatArray, bx: FloatArray, by: FloatArray, options: Options = Options(), initial: IntArray? = null): Result Matches source points (ax[i](workspace.md#stdlib-kt--i), ay[i](workspace.md#stdlib-kt--i)) onto target points (bxj, byj).
fun validatePermutation(p: IntArray, n: Int = p.size) Throws IllegalArgumentException unless p is a permutation of 0 until n.

lp_fan_map.kt

The central-path fan as a map from the (θ, log μ) plane into a polygon.

For an objective direction c(θ) = (cos θ, sin θ) and barrier weight μ, the barrier maximiser x(θ, μ) solves c(θ) = μ Σ wᵢ nᵢ / sᵢ(x). Write g(x) = Σ wᵢ nᵢ / sᵢ(x): the gradient of the (negated) barrier, a diffeomorphism from the polygon's interior onto the whole plane. Then g(x) = c(θ)/μ, so (θ, 1/μ) are polar coordinates of g(x) and the fan is the polar grid pulled back through g⁻¹: one-to-one onto the interior minus the weighted analytic centre (g = 0).

Differentiating the optimality condition with M = μ Σ wᵢ nᵢnᵢᵀ / sᵢ² (the negated Hessian):

∂x/∂θ     =  M⁻¹ c'(θ)
∂x/∂log μ = −M⁻¹ c(θ)

and det[∂x/∂θ, ∂x/∂log μ] = det(M⁻¹) · det[c', −c] = 1 / det M > 0. The map never folds; it only pinches, at both ends of the log μ axis.

src/jvmMain/kotlin/afield/geometry/spatial/lp_fan_map.kt — read the source

Used in: Linear programs as curves

Top-level functions

Declaration Summary
fun LpPolygon.fanMap(weights: DoubleArray? = null): LpFanMap This polygon's central-path fan as a map x(θ, log μ); see LpFanMap.

LpFanPoint

class LpFanPoint(val theta: Double, val logMu: Double, val x: Double, val y: Double, val dxdTheta: Vec2, val dxdLogMu: Vec2, val newtonSteps: Int)

One point of an LpFanMap: where (theta, logMu) lands in the polygon, and the map's Jacobian there.

Declaration Summary
val theta: Double The objective direction's angle, radians.
val logMu: Double The natural logarithm of the barrier weight μ.
val x: Double The barrier maximiser's x.
val y: Double The barrier maximiser's y.
val dxdTheta: Vec2 ∂x/∂θ, how the point moves as the direction turns: M⁻¹ c'(θ).
val dxdLogMu: Vec2 ∂x/∂log μ, how it moves as μ grows: −M⁻¹ c(θ). Its negative points along the central path toward the optimal vertex.
val newtonSteps: Int The Newton steps this point took from its starting guess.
val position: Vec2 The point, for drawing.
val jacobianDeterminant: Double det[∂x/∂θ, ∂x/∂log μ]: the area in the polygon that a unit of (θ, log μ) area covers here. Always positive, and equal to 1 / det M; it falls toward 0 near the analytic centre (large μ) and near the vertices (small μ), where many parameter values crowd into little room.
override fun toString(): String

LpFanMap

class LpFanMap(val polygon: LpPolygon, weights: DoubleArray? = null)

The fan of central paths as a smooth map x(θ, log μ) from a parameter plane into polygon: θ is the objective direction's angle and log μ the barrier weight's logarithm. Each value is the barrier maximiser, found by damped Newton from a starting guess; give it a nearby point (at with near, or let grid and warp chain their own) and it takes one or two steps.

Declaration Summary
val polygon: LpPolygon The polygon the map lands in.
val weights: DoubleArray Each edge's barrier weight, as for centralPath.
fun at(theta: Double, logMu: Double, start: Vec2 = polygon.center): LpFanPoint The point for direction angle theta and barrier weight exp(logMu), with Newton started at start (strictly inside; the polygon's centre by default).
fun at(theta: Double, logMu: Double, near: LpFanPoint): LpFanPoint The point for theta and logMu, with Newton started from a neighbouring point near.
fun grid(thetas: Int = 96, logMus: Int = 48, logMuHigh: Double = ln(100.0), logMuLow: Double = ln(1e-3), startAngle: Double = 0.0): LpFanGrid The map sampled on a thetas × logMus grid: θ in equal steps round the whole circle from startAngle, log μ in equal steps from logMuHigh down to logMuLow. Each point starts Newton from whichever of its two solved neighbours (the previous angle at this μ, or this angle at the previous μ) Newton judges closer, which takes a little over half the steps cold starts would (LpFanGrid.newtonSteps).
fun warp(line: FLine, stepsPerSegment: Int = 16): FLine Carry line, drawn in the parameter plane with x as θ (radians) and y as log μ, into the polygon. Each segment is cut into stepsPerSegment pieces before mapping (cubic segments are evaluated along their curve), since a straight segment in the parameter plane is a curve in the polygon; subpaths stay separate. Newton for each point starts from the last. Colour and thickness are copied from line.

LpFanGrid

class LpFanGrid(val thetas: DoubleArray, val logMus: DoubleArray, val points: List<List<LpFanPoint>>)

The fan map sampled on a grid, from LpFanMap.grid. pointsiis angle `thetas[`i`](workspace.md#stdlib-kt--i)` and level `logMus`j (largest μ first).

Declaration Summary
val newtonSteps: Int Newton steps the whole grid took.
val minDeterminant: Double The smallest Jacobian determinant on the grid; it is at the small-μ or large-μ edge.
fun thetaLines(every: Int = 1, alpha: Double = 0.85, thickness: Double = 1.0): List<FLine> The θ lines, every everyth angle: each is a central path from the large-μ end to the small, coloured round a hue circle as toFLines colours a fan.
fun muLines(every: Int = 1, color: Vec4 = vec(1.0, 1.0, 1.0, 0.45), thickness: Double = 0.8): List<FLine> The μ lines, every everyth level: closed rings round the analytic centre.

lp_polytope.kt

The three-dimensional companion of lp_trajectories.kt: the same linear-programming methods, as curve generators, on a convex polytope { x : nᵢ·x ≤ bᵢ } in space. The methods are the 2-D ones with 3×3 linear algebra in place of 2×2 (the ellipsoid method's constants change with the dimension: centre step 1/(n+1) = 1/4, expansion n²/(n²−1) = 9/8). The simplex walk follows the polytope's edges, which the constructor finds by enumerating vertices.

Pure math, independent of Box; doubles throughout, Vec3 only for drawing.

src/jvmMain/kotlin/afield/geometry/spatial/lp_polytope.kt — read the source

Used in: Linear programs as curves

Top-level functions

Declaration Summary
fun LpPolytope.centralPath(direction: Vec3, weights: DoubleArray? = null, samples: Int = 160, muHigh: Double = 1000.0, muLow: Double = 1e-5, start: Vec3 = center): LpTrajectory3 The log-barrier central path in 3-D: for μ falling geometrically from muHigh to muLow, the maximiser of c·x + μ Σ wᵢ ln(slackᵢ), by damped Newton (a 3×3 solve per step) warm-started from the previous sample. It leaves the weighted analytic centre and ends at the optimal vertex for direction. Every sample is strictly feasible; LpSample3.scale is μ. See the 2-D LpPolygon.centralPath for what the weights do.
fun sphereDirections(count: Int): List<Vec3> count directions spread evenly over the unit sphere (a Fibonacci spiral), the directions LpPolytope.centralFan uses.
fun LpPolytope.centralFan(directions: Int = 64, samples: Int = 100, weights: DoubleArray? = null, muHigh: Double = 1000.0, muLow: Double = 1e-5): List<LpTrajectory3> A fan of central paths over directions objective directions spread over the sphere (sphereDirections): every path leaves the same weighted analytic centre and ends at the vertex that wins for its direction.
fun LpPolytope.primalDual(direction: Vec3, maxIterations: Int = 60, tolerance: Double = 1e-9, stepFraction: Double = 0.95, start: Vec3 = center): LpTrajectory3 Mehrotra's predictor–corrector primal-dual interior-point method in 3-D, every Newton iterate recorded; the 3×3 port of LpPolygon.primalDual. LpSample3.scale is the duality measure μ.
fun LpPolytope.pdhg(direction: Vec3, iterations: Int = 1000, tau: Double = 0.15, sigma: Double = 0.15, theta: Double = 1.0, start: Vec3 = center): LpPdhgTrace3 Primal-dual hybrid gradient (Chambolle–Pock) in 3-D, every primal and dual state kept, infeasible ones included; the port of LpPolygon.pdhg. The primal walk overshoots the polytope and spirals into the optimal vertex. LpSample3.scale is the dual vector's norm.
fun LpPolytope.ellipsoid(direction: Vec3, iterations: Int = 120, initialRadius: Double? = null, start: Vec3 = center): LpEllipsoidTrace3 The central-cut ellipsoid method in 3-D, every centre and ellipsoid kept. It starts from a ball of initialRadius (default: the polytope's diameter) around start, cuts on the most violated half-space at an infeasible centre and on the objective at a feasible one. In n = 3 dimensions each cut moves the centre by −Pg / (4√(gᵀPg)) and replaces P by 9/8 · (P − ½ (Pg)(Pg)ᵀ / gᵀPg) (n²/(n²−1) and 2/(n+1)), which multiplies the volume by 27/32 ≈ 0.84 every step: slower than the 2-D method's 0.77, as the method is in higher dimensions. LpSample3.tag and LpSample3.scale (√det P) are as in LpPolygon.ellipsoid.
fun LpPolytope.simplexWalk(direction: Vec3, startVertex: Int = 0): LpTrajectory3 The simplex method in 3-D: a walk along the polytope's LpPolytope.edges from startVertex, moving each time to the neighbouring vertex that improves c·x most, until none does. A convex polytope has no local optimum that is not global, so it ends at the optimal vertex. LpSample3.scale is NaN.
fun LpPolytope.cuttingPlanes(direction: Vec3, iterations: Int = 20, center: LpCenter = LpCenter.ANALYTIC, minVolume: Double = 1e-9): LpCuttingPlaneTrace3 A cutting-plane walk in 3-D: find a centre of the region still in play, cut it with the objective (c·x ≥ c·centre), repeat. center is LpCenter.ANALYTIC (the analytic centre of every constraint so far, cuts included), LpCenter.CENTROID (the centre of gravity, which removes at least 1 − (3/4)³ ≈ 58% of the volume each step) or LpCenter.CHEBYSHEV (the centre of the largest inscribed ball) or LpCenter.VOLUMETRIC (Vaidya's volumetric centre of every constraint so far, started from the analytic centre). Stops when the remaining volume falls below minVolume or after iterations. LpSample3.scale is the remaining volume, and LpCuttingPlaneTrace3.regions holds each region.
fun List<LpTrajectory3>.toFLines(alpha: Double = 0.8, thickness: Double = 1.1): List<FLine> Every path of a 3-D fan as an FLine, coloured round a hue circle.

LpPolytope

class LpPolytope(normals: List<Vec3>, bounds: List<Double>)

A bounded convex polytope with an interior, held as its half-spaces nᵢ·x ≤ bᵢ (unit outward normals) and as the vertices and edges those half-spaces make, ready for the trajectory methods (centralPath, centralFan, primalDual, pdhg, ellipsoid, simplexWalk, cuttingPlanes).

Declaration Summary
val normalX: DoubleArray x component of each half-space's unit outward normal.
val normalY: DoubleArray y component of each half-space's unit outward normal.
val normalZ: DoubleArray z component of each half-space's unit outward normal.
val bound: DoubleArray Each half-space's bound: the polytope is normal[i](workspace.md#stdlib-kt--i)·x ≤ bound[i](workspace.md#stdlib-kt--i) for every i.
val vertices: List<Vec3> The vertices, each where three or more of the planes meet.
val vertexPlanes: List<IntArray> For each vertex, the indices of the half-spaces tight at it.
val edges: List<IntArray> The edges, as pairs of indices into vertices.
val center: Vec3 The mean of the vertices, strictly inside; the default start.
val constraintCount: Int The number of half-spaces, which is the number of constraints.
fun slack(i: Int, x: Double, y: Double, z: Double): Double How far (x, y, z) is inside half-space i: positive inside, zero on its plane.
fun violation(x: Double, y: Double, z: Double): Double The largest constraint violation at (x, y, z), zero inside or on the polytope.
fun containsStrictly(x: Double, y: Double, z: Double): Boolean True when (x, y, z) is strictly inside every half-space.
fun optimalVertex(direction: Vec3): Vec3 The vertex that maximises direction · x.
fun neighbours(i: Int): List<Int> The vertices next to vertex i along an edge.
fun toFLine(color: Vec4 = vec(0.62, 0.70, 0.80, 0.9), thickness: Double = 1.4): FLine The edges as one FLine of separate segments: the polytope's wireframe.
fun toMesh(): TriMesh The solid, as a triangulated TriMesh (the hull of vertices).
val volume: Double The enclosed volume.

LpSample3

data class LpSample3(val x: Double, val y: Double, val z: Double, val iteration: Int, val objective: Double, val violation: Double, val scale: Double, val tag: String)

One step of a 3-D trajectory: the 3-D LpSample.

Declaration Summary
val x: Double The iterate's x.
val y: Double The iterate's y.
val z: Double The iterate's z.
val iteration: Int The step number, starting at 0.
val objective: Double c·x with c the normalised objective direction.
val violation: Double The largest constraint violation, zero for a feasible iterate.
val scale: Double The method's own measure, as for LpSample: μ for centralPath and primalDual, the dual norm for pdhg, √det P for ellipsoid, the remaining volume for cuttingPlanes, NaN for simplexWalk.
val tag: String What kind of step this is: the method's name, or for ellipsoid which cut was taken — feasibility-cut, objective-cut, or final at the last step.
val position: Vec3 The iterate as a Vec3, for drawing.

LpTrajectory3

class LpTrajectory3(val method: String, val samples: List<LpSample3>, val solverSteps: Int = 0)

A 3-D method's whole walk, in order; see LpTrajectory.

Declaration Summary
val last: LpSample3 The final iterate.
val positions: List<Vec3> The iterates as Vec3s.
fun toFLine(color: Vec4 = vec(1.0, 0.72, 0.32, 0.95), thickness: Double = 1.6): FLine The walk as an open 3-D polyline.

LpEllipsoid

data class LpEllipsoid(val cx: Double, val cy: Double, val cz: Double, val pxx: Double, val pxy: Double, val pxz: Double, val pyy: Double, val pyz: Double, val pzz: Double)

The ellipsoid { z : (z − c)ᵀ P⁻¹ (z − c) ≤ 1 } with centre (cx, cy, cz) and symmetric positive definite P = [[pxx, pxy, pxz], [pxy, pyy, pyz], [pxz, pyz, pzz]], as kept by LpPolytope.ellipsoid.

Declaration Summary
val cx: Double, val cy: Double, val cz: Double
val pxx: Double, val pxy: Double, val pxz: Double, val pyy: Double, val pyz: Double, val pzz: Double
val determinant: Double det P; the volume is 4/3 π √det.
val isPositiveDefinite: Boolean True when P is symmetric positive definite (all leading minors positive).
fun quadraticForm(x: Double, y: Double, z: Double): Double (z − c)ᵀ P⁻¹ (z − c) for z = (x, y, z): at most 1 inside the ellipsoid.
fun toFLine(rings: Int = 6, segments: Int = 48, color: Vec4 = vec(0.55, 0.80, 1.0, 0.4), thickness: Double = 0.8): FLine A wireframe: the unit sphere's rings circles of latitude and rings meridians, mapped onto the ellipsoid through the Cholesky factor of P, as one FLine.

LpEllipsoidTrace3

class LpEllipsoidTrace3(val trajectory: LpTrajectory3, val ellipsoids: List<LpEllipsoid>)

LpPolytope.ellipsoid's result: the centres, and the ellipsoid kept at every step.

LpPdhgTrace3

class LpPdhgTrace3(val trajectory: LpTrajectory3, val duals: List<DoubleArray>)

LpPolytope.pdhg's result: the primal walk, and the dual vector at every step.

LpCuttingPlaneTrace3

class LpCuttingPlaneTrace3(val trajectory: LpTrajectory3, val regions: List<LpPolytope>)

LpPolytope.cuttingPlanes's result: the centres, and the region still in play at every step.

lp_trajectories.kt

Linear-programming algorithms on a convex polygon, used as curve generators.

A 2-D linear program is "maximise c·x over a convex polygon". Every classical method for it walks from somewhere to the same optimal vertex, and each walks differently: the barrier method's central path is a smooth arc from the analytic centre, the primal-dual method takes a few long Newton strides, PDHG spirals in, the ellipsoid method shrinks a sequence of ellipses, the simplex method hops along the boundary and cutting planes carve the region away. Here those walks are the product. Every method returns its whole trajectory with per-step metadata (LpSample), and each result type has a toFLine that draws it, with toFLines for a whole fan of them.

The pure math is independent of Box and of FLine: an LpPolygon is a list of half-planes and the methods are plain functions on it. Doubles throughout; LpSample.position is the Vec2 for drawing. The visual reference for all of this is https://lpviz.net.

src/jvmMain/kotlin/afield/geometry/spatial/lp_trajectories.kt — read the source

Used in: Linear programs as curves

Top-level functions

Declaration Summary
fun LpPolygon.centralPath(direction: Vec2, weights: DoubleArray? = null, samples: Int = 160, muHigh: Double = 1000.0, muLow: Double = 1e-5, start: Vec2 = center, warm: LpTrajectory? = null): LpTrajectory The log-barrier central path: for μ falling geometrically from muHigh to muLow, the maximiser of c·x + μ Σ wᵢ ln(slackᵢ), found by damped Newton warm-started from the previous sample. At large μ it sits at the weighted analytic centre; as μ falls it bends toward the optimal vertex for direction, which it approaches as μ → 0.
fun LpPolygon.centralFan(angles: Int = 48, samples: Int = 120, weights: DoubleArray? = null, startAngle: Double = 0.0, muHigh: Double = 1000.0, muLow: Double = 1e-5): List<LpTrajectory> A fan of central paths: the objective direction turned through a full circle in angles equal steps, starting at startAngle radians, one centralPath each. Every path leaves the same weighted analytic centre and ends at the vertex that is optimal for its direction, so the fan is a sampling of the surface x(θ, μ).
fun LpPolygon.primalDual(direction: Vec2, maxIterations: Int = 60, tolerance: Double = 1e-9, stepFraction: Double = 0.95, start: Vec2 = center): LpTrajectory Mehrotra's predictor–corrector primal-dual interior-point method, recording every Newton iterate. Primal x starts at start with slacks s = b − Ax, so it stays strictly feasible; the duals start at 1 and the dual residual Aᵀy − c is driven to zero alongside the duality measure μ = sᵀy / m. Each step is the 2×2 normal-equation solve, with separate primal and dual step lengths at fraction stepFraction of the distance to the boundary. Stops when μ and the dual residual are below tolerance, or after maxIterations. LpSample.scale is μ.
fun LpPolygon.pdhg(direction: Vec2, iterations: Int = 1000, tau: Double = 0.15, sigma: Double = 0.15, theta: Double = 1.0, start: Vec2 = center): LpPdhgTrace Primal-dual hybrid gradient (Chambolle–Pock) on the saddle problem max_x min_{y ≥ 0} c·x − yᵀ(Ax − b), keeping every primal and dual state, infeasible ones included. Each step projects the dual y ← max(0, y + σ(A x̄ − b)), moves the primal x ← x + τ(c − Aᵀy), and extrapolates x̄ = x + θ(x − x_prev).
fun LpPolygon.ellipsoid(direction: Vec2, iterations: Int = 80, initialRadius: Double? = null, start: Vec2 = center): LpEllipsoidTrace The central-cut ellipsoid method, returning every centre and every ellipse. It starts from a disc of initialRadius (default: the polygon's diameter) around start. At an infeasible centre it cuts on the most violated edge; at a feasible one it cuts on the objective, keeping the half where c·x is at least its current value. Each central cut in two dimensions moves the centre by −Pg / (3√(gᵀPg)) and replaces P by 4/3 · (P − 2/3 · (Pg)(Pg)ᵀ / gᵀPg), which multiplies the area by √(16/27) ≈ 0.77 every step. LpSample.tag is "feasibility-cut" or "objective-cut" for the cut taken at that centre ("final" for the last), and LpSample.scale is √det P.
fun LpPolygon.simplexWalk(direction: Vec2, startVertex: Int = 0): LpTrajectory The simplex method in two dimensions: a walk along the boundary from startVertex, moving to whichever neighbouring vertex improves c·x more, until neither does. It ends at the optimal vertex, having visited at most half the polygon. LpSample.scale is NaN.
fun LpPolygon.cuttingPlanes(direction: Vec2, iterations: Int = 40, center: LpCenter = LpCenter.ANALYTIC, minArea: Double = 1e-10): LpCuttingPlaneTrace A cutting-plane walk: find a centre of the region still in play, cut the region with the objective (c·x ≥ c·centre), and repeat. With LpCenter.ANALYTIC the centre is the analytic centre of every constraint so far, cuts included, so earlier cuts keep pushing on the path just as extra constraints push a central path; with LpCenter.CENTROID it is the centre of gravity, which removes at least 1 − (2/3)² of the area each step; with LpCenter.CHEBYSHEV it is the centre of the largest inscribed disc, and with LpCenter.VOLUMETRIC Vaidya's volumetric centre of every constraint so far. The trajectory approaches the optimal vertex from inside; it stops when the remaining area falls below minArea or after iterations. LpSample.scale is the remaining area, and LpCuttingPlaneTrace.regions holds the region at every step.
fun List<LpTrajectory>.toFLines(alpha: Double = 0.8, thickness: Double = 1.1): List<FLine> Every path of a fan as an FLine, coloured round a hue circle so neighbouring angles read apart.

LpPolygon

class LpPolygon(points: List<Vec2>)

A strictly convex polygon, held both as its vertices and as the half-planes nᵢ·x ≤ bᵢ of its edges, ready for the trajectory methods (centralPath, centralFan, primalDual, pdhg, ellipsoid, simplexWalk, cuttingPlanes).

Declaration Summary
val vertices: List<Vec2> The vertices, counter-clockwise, without a closing repeat.
val normalX: DoubleArray x component of each edge's unit outward normal.
val normalY: DoubleArray y component of each edge's unit outward normal.
val bound: DoubleArray Each edge's bound: the polygon is normalX[i](workspace.md#stdlib-kt--i)·x + normalY[i](workspace.md#stdlib-kt--i)·y ≤ bound[i](workspace.md#stdlib-kt--i) for every i.
val center: Vec2 The mean of the vertices, strictly inside a strictly convex polygon; the default start.
val edgeCount: Int The number of edges, which is also the number of constraints and of vertices.
fun slack(i: Int, x: Double, y: Double): Double How far (x, y) is inside edge i: positive inside, zero on the edge, negative outside.
fun violation(x: Double, y: Double): Double The largest constraint violation at (x, y), zero for any point inside or on the polygon.
fun containsStrictly(x: Double, y: Double): Boolean True when (x, y) is strictly inside every edge.
fun optimalVertex(direction: Vec2): Vec2 The vertex that maximises direction · x: the answer every method here walks toward.
fun toFLine(color: Vec4 = vec(0.62, 0.70, 0.80, 0.9), thickness: Double = 1.4): FLine The polygon's outline as a closed FLine.

LpSample

data class LpSample(val x: Double, val y: Double, val iteration: Int, val objective: Double, val violation: Double, val scale: Double, val tag: String)

One step of a trajectory.

Declaration Summary
val x: Double The iterate's x.
val y: Double The iterate's y.
val iteration: Int The step number, starting at 0.
val objective: Double c·x with c the normalised objective direction.
val violation: Double The largest constraint violation, zero for a feasible iterate.
val scale: Double The method's own measure at this step: the barrier weight μ for centralPath, the duality measure μ for primalDual, the dual norm for pdhg, √det of the ellipse for ellipsoid, the remaining area for cuttingPlanes, and NaN for simplexWalk.
val tag: String What kind of step this is: the method's name, or for ellipsoid which cut was taken — feasibility-cut, objective-cut, or final at the last step.
val position: Vec2 The iterate as a Vec2, for drawing.

LpTrajectory

class LpTrajectory(val method: String, val samples: List<LpSample>, val solverSteps: Int = 0)

A method's whole walk, in order. method names the method that produced it.

Declaration Summary
val last: LpSample The final iterate.
val positions: List<Vec2> The iterates as Vec2s.
fun toFLine(color: Vec4 = vec(1.0, 0.72, 0.32, 0.95), thickness: Double = 1.6): FLine The walk as an open polyline.

LpEllipse

data class LpEllipse(val cx: Double, val cy: Double, val pxx: Double, val pxy: Double, val pyy: Double)

The ellipse { z : (z − c)ᵀ P⁻¹ (z − c) ≤ 1 } with centre (cx, cy) and symmetric positive definite shape matrix P = [[pxx, pxy], [pxy, pyy]], as kept by ellipsoid.

Declaration Summary
val determinant: Double det P; the ellipse's area is π √det.
val isPositiveDefinite: Boolean True when the shape matrix is symmetric positive definite, i.e. this is a real ellipse.
fun toFLine(segments: Int = 72, color: Vec4 = vec(0.55, 0.80, 1.0, 0.5), thickness: Double = 1.0): FLine The boundary as a closed FLine of segments chords, through the Cholesky factor of P.

LpEllipsoidTrace

class LpEllipsoidTrace(val trajectory: LpTrajectory, val ellipses: List<LpEllipse>)

ellipsoid's result: the centres, and the ellipse kept at every step (same length).

LpPdhgTrace

class LpPdhgTrace(val trajectory: LpTrajectory, val duals: List<DoubleArray>)

pdhg's result: the primal walk, and the dual vector (one entry per edge) at every step.

LpCuttingPlaneTrace

class LpCuttingPlaneTrace(val trajectory: LpTrajectory, val regions: List<List<Vec2>>)

cuttingPlanes's result: the centres, and the region still in play at every step.

LpCenter

enum class LpCenter

The centres cuttingPlanes can cut through.

Constants: ANALYTIC, CENTROID, CHEBYSHEV, VOLUMETRIC.

LpContinuation

class LpContinuation

A warm-started centralPath for a polygon, direction or weights that move smoothly between calls — an animation's frames. From the third call on it extrapolates each sample linearly from the previous two paths (2·last − before, a secant predictor) and passes that as centralPath's warm; on the second call it passes the previous path itself. Newton then starts next to the answer and needs fewer steps than following the path cold, for the same path. last is the latest path and lastSteps its Newton step count; reset forgets the history, which a jump should do. A prediction that does not fit (a different sample count, or a sample Newton judges further off than the path's own previous sample) is ignored, so a jump costs a cold start and nothing else.

Declaration Summary
var last: LpTrajectory? The path the previous call returned, or null before the first call and after reset.
val lastSteps: Int Newton steps the previous call took.
fun centralPath(polygon: LpPolygon, direction: Vec2, weights: DoubleArray? = null, samples: Int = 160, muHigh: Double = 1000.0, muLow: Double = 1e-5): LpTrajectory polygon's central path for direction, warm-started from the previous calls'.
fun reset() Forget the previous paths, so the next call starts cold.

spatial_field.kt

The bridges out of Field's plane-geometry results and into the things Field already draws.

Every partitioner in graph_connectivity.kt, dirichlet.kt, contours.kt and clustering.kt answers with structure — cell vertex rings, circular arcs, triangle index triples, cluster labels — and none of them answers with geometry. This file converts each of them: one toFLines per result type, so a Voronoi diagram, an Apollonius cell, a Delaunay triangulation or a k-medoids partition lands in the same drawing, animation and export machinery as anything drawn by hand. It is the plane counterpart of mesh_field.kt, which does the same job for TriMesh.

Two layers:

  1. Structure → FLine. PointGraph.toFLine, delaunayTrianglesToFLine, VoronoiDiagram.toFLines, CircularDirichlet.Diagram.toFLines, CircularDirichlet.CellBoundary.toFLines, ClusterLayer.toFLines and pointCloudFLine. Each returns styled, drawable lines and nothing else — no Box, no output, so they are usable from a test or a renderer as readily as from a box.
  2. FLine → a captioned figure. Box.showPlane and Box.showPlaneGrid rasterise lines to inline SVG with drawToSVG and push a titled card — live and pannable for one figure, frozen for a grid of them — to a box output channel: the fastest way to see a whole pipeline at once, and the 2D twin of Box.showMeshGrid. planeFigureHtml is the markup on its own.

The figure helpers are AWT-free (Skia's SVG canvas, same as Box.lines), so they work headless, including inside the docs build.

src/jvmMain/kotlin/afield/geometry/spatial/spatial_field.kt — read the source

Used in: Points into structure — Voronoi, Apollonius, contours and clusters, Linear programs as curves, Evolving Creatures

Top-level functions

Declaration Summary
fun pointCloudFLine(points: List<Vec2>, color: Vec4 = vec(1.0, 1.0, 1.0, 0.85), pointSize: Double = 1.2, z: Number = 0): FLine The points themselves as one dot-drawn FLine — the layer every other figure here sits on top of.
fun PointGraph.toFLine(points: List<Vec2>, color: Vec4 = vec(0.55, 0.68, 0.82, 0.55), thickness: Double = 1.4, z: Number = 0): FLine Every edge of this graph as one FLine, with points supplying the positions.
fun delaunayTrianglesToFLine(points: List<Vec2>, triangles: List<Triple<Int, Int, Int>>, color: Vec4 = vec(0.62, 0.74, 0.88, 0.45), thickness: Double = 1.2, filled: Boolean = false, z: Number = 0): FLine The triangles of a triangulation as one FLine of closed triangle outlines.
fun VoronoiDiagram.toFLines(color: Vec4 = vec(0.72, 0.82, 0.95, 0.7), palette: List<Vec4>? = null, thickness: Double = 1.3, filled: Boolean = false, z: Number = 0): List<FLine> One closed FLine per Voronoi cell, in cell order.
fun CircularDirichlet.CellBoundary.toFLines(clip: CircularDirichlet.Aabb? = null, maxChordError: Float = 0.02f, color: Vec4 = vec(0.8, 0.86, 0.98, 0.8), thickness: Double = 1.4, z: Number = 0): List<FLine> The boundary of one multiplicatively-weighted Voronoi cell, tessellated into FLines.
fun CircularDirichlet.Diagram.toFLines(clip: CircularDirichlet.Aabb? = null, maxChordError: Float = 0.02f, color: Vec4 = vec(0.8, 0.86, 0.98, 0.8), palette: List<Vec4>? = null, thickness: Double = 1.4, z: Number = 0): List<FLine> Every cell of a weighted diagram, tinted per site.
fun ClusterLayer.toFLines(points: List<Vec2>, centers: List<Vec2>? = null, palette: List<Vec4> = okhslSet(maxOf(1, clusterCount), saturation = 0.78, lightness = 0.62), pointSize: Double = 1.3, centerSize: Double = 3.0, drawSpokes: Boolean = true, spokeAlpha: Float = 0.18f, z: Number = 0): List<FLine> A cluster assignment drawn over the points it labels: one dot layer per cluster, plus optional spokes from each sample to its cluster's centre.
fun planeFigureHtml(lines: List<FLine>, title: String? = null, caption: String? = null, width: Int = 640, height: Int = 460, padding: Double = 14.0, window: Rect? = null, interactive: Boolean = false, yUp: Boolean = false): String The markup for one titled figure: lines fitted into a width × height frame, rasterised to inline SVG, on the same dark card the rest of Field's inline figures use.
fun Box.showPlane(lines: List<FLine>, channel: String = "main", title: String? = null, caption: String? = null, width: Int = 720, height: Int = 520, padding: Double = 14.0, window: Rect? = null, interactive: Boolean = true, yUp: Boolean = false) Push one plane figure to a box output channel.
fun Box.showPlaneGrid(vararg panels: Pair<String, List<FLine>>, channel: String = "main", columns: Int = 2, cellWidth: Int = 420, cellHeight: Int = -1, padding: Double = 12.0, window: Rect? = null, yUp: Boolean = false) Several plane figures side by side, each titled — the 2D twin of Box.showMeshGrid, and the fastest way to see a whole pipeline (sites → triangulation → dual → clipped) side by side.

stippling.kt

Stippling: place exactly N points so that they follow an image's brightness and keep an even, low-correlation ("blue-noise-ish") spacing from one another.

val dots = imgFromFile("face.png").stipple(5000)        // List<Vec2>, pixel coordinates

Sampling the brightness as a probability distribution gets the density right but clumps: two independent samples are as likely to land on top of each other as anywhere else. This file instead computes a weighted centroidal Voronoi tessellation (Secord's stippling) by Lloyd relaxation on the CPU. Each iteration

  1. rasterises the points into the pixel grid,
  2. finds every pixel's nearest point with a separable exact Euclidean distance transform (a two-scan nearest-seed pass along each row, then Felzenszwalb–Huttenlocher's lower envelope of parabolas down each column), which yields the Voronoi cells as a label raster in O(pixels) regardless of the point count,
  3. moves each point to the density-weighted centroid of its cell.

The rows and columns are split across a fork-join pool of their own, and nothing on the per-iteration path allocates: the label rasters, the column scratch and the per-worker centroid accumulators are all allocated once, in the constructor.

Where the points go, and what relaxation does not change

The number of points in a region is decided by the starting sample. Relaxation evens out the spacing between neighbours and moves hardly any points from one region to another, so the point density is whatever the start gave, brightness^densityPower. With the default of 1, points follow brightness, and a larger power exaggerates contrast.

That makes the start matter. An independent weighted random sample has the right density on average and random clumps everywhere, and relaxation keeps the clumps' counts: on a flat field the count variance in 48-pixel windows is 0.60 of a random scatter's after relaxing, which reads as blotches in a mid tone. So the start is stratified along a Hilbert curve instead. The curve visits the image in a way that keeps nearby pixels nearby, it is cut into count stretches of equal density, and each stretch gets one point. Every region the curve covers then starts with its exact share, and the same measurement after relaxing is 0.028.

Theory predicts a different density: the optimal centroidal tessellation of a density ρ in 2-D has point density ρ^(1/2). Lloyd iteration from a weighted start does not travel there. On a field half at brightness 1 and half at 0.5 the bright/dim count ratio stays at 2 through hundreds of iterations, against an optimum of 1.41, until the iteration reaches a fixed point on the raster where Stippler.step returns 0. StipplingTest pins both powers, the drift and the lumpiness.

Memory

Besides five rasters of supersample² * width * height pixels, each worker keeps its own three DoubleArray(count) centroid accumulators so that workers never contend. That is cheap at ten or a hundred thousand points, and 384 MB at a million points on 16 workers — pass a smaller threads there.

src/jvmMain/kotlin/afield/geometry/spatial/stippling.kt — read the source

Top-level functions

Declaration Summary
fun Img.stipple(count: Int, iterations: Int = 20, densityPower: Float = 1f, invert: Boolean = false, seed: Int = 1): List<Vec2> count stipple points following this image's brightness, after iterations rounds of weighted Lloyd relaxation. Points are in pixel coordinates: origin top-left, y down, a pixel's centre at +0.5. Use Stippler directly to relax incrementally, for instance one step per frame.
fun stipplesToFLine(points: List<Vec2>, size: Double = 1.0, z: Number = 0): FLine Stipple points as one pointed FLine, ready for lines(...): a moveTo per point and no segments, so it draws dots and nothing between them.
fun stipplesToImg(points: List<Vec2>, width: Int, height: Int, radius: Double = 1.0, ink: Boolean = true): Img Stipple points rasterised as anti-aliased discs: a 1-channel Img, for showImg(...).

Stippler

class Stippler(image: Img, val count: Int, densityPower: Float = 1f, invert: Boolean = false, val seed: Int = 1, threads: Int = Runtime.getRuntime().availableProcessors(), supersample: Int = 0)

An incremental weighted Lloyd relaxation of count points over an image's brightness — the engine behind stipple, kept as an object so a box can watch it settle:

Declaration Summary
val count: Int How many points; points always returns exactly this many.
val seed: Int The seed for the initial placement and for re-placing empty cells.
val width The image's width in pixels; points have x in 0..width.
val height The image's height in pixels; points have y in 0..height.
val supersample: Int How many times finer than the image the tessellation is computed. The centroid of a cell a few pixels across is quantised to the pixel grid, and in dense regions that lines the dots up into chains. So unless asked otherwise this picks the factor, up to 4, that gives an average cell at least 128 raster pixels. Points are reported in image pixels regardless.
val xs Point i's x, in pixels. Written by step; read it directly in a hot loop.
val ys Point i's y, in pixels, y down. Written by step.
var iterations How many steps have run.
fun points(): List<Vec2> The points as values, in pixel coordinates. Allocates a fresh list; see xs / ys to avoid it.
fun relax(iterations: Int = 20, tolerance: Double = 0.0): Stippler Run up to iterations steps, stopping early once the mean movement falls to tolerance pixels or below. Returns this, so Stippler(img, n).relax(20).points() reads in one line.
fun step(): Double One Lloyd iteration: Voronoi cells by distance transform, then every point to its cell's density-weighted centroid. Returns the mean distance the points moved, in pixels — it falls towards zero as the tessellation settles, which makes it the thing to plot.

transport.kt

Optimal transport between point sets: which source goes to which target, or how much of each source goes to each target, at the least total cost. Three solvers, from exact and small to approximate and large:

  • assignment (the Hungarian method) answers the one-to-one question exactly, for two equally sized sets. It is O(n³) over an n × n cost matrix, so it is for hundreds of points, maybe a few thousand. Flow matching's minibatch coupling and the FLine trajectory matcher use it.
  • matchPoints (BSP-OT, in bspot.kt) answers the same one-to-one question approximately, for 2D clouds of a million points a side.
  • sinkhorn answers a softer question exactly: it transports mass, so the sets may differ in size and carry weights, and every source may split between several targets. The answer is a transport plan rather than a pairing. A small sinkhorn epsilon makes the plan nearly a permutation and its cost nearly the assignment cost; a large one blurs it.

Entropic transport

sinkhorn solves the entropy-regularised problem: minimise Σ πᵢⱼ Cᵢⱼ + ε Σ πᵢⱼ (ln πᵢⱼ − 1) over plans π whose rows sum to the source weights a and whose columns sum to the target weights b. The entropy term makes the problem strictly convex, so the answer is unique, smooth in the inputs, and found by alternately rescaling rows and columns (Cuturi 2013). This implementation keeps the scalings as log-domain dual potentials f, g, so a small ε does not underflow, and πᵢⱼ = exp((fᵢ + gⱼ − Cᵢⱼ) / ε).

ε is in the units of the cost. With squared-distance costs it is an area: ε = 0.01 on points in a unit square blurs over roughly √0.01 = 0.1. Each iteration is O(n·m), and smaller ε needs more of them; multiscaleSinkhorn solves a coarse version first and starts the fine one from it.

src/jvmMain/kotlin/afield/geometry/spatial/transport.kt — read the source

Used in: Optimal Transport

Top-level functions

Declaration Summary
fun assignment(cost: Array<DoubleArray>): IntArray The exact one-to-one assignment for a square cost matrix by the Hungarian method: result[i](workspace.md#stdlib-kt--i) is the column row i is matched to, and the total of cost[i](workspace.md#stdlib-kt--i)[result[i](workspace.md#stdlib-kt--i)] is the least possible. O(n³); an empty matrix gives an empty result.
fun assignment(from: List<Vec2>, to: List<Vec2>): IntArray The exact one-to-one matching of from onto to that minimises total squared distance: result[i](workspace.md#stdlib-kt--i) is the index in to that from[i](workspace.md#stdlib-kt--i) goes to. The two lists must be the same size. For more than a few thousand points use matchPoints, which is approximate and O(n log n).
fun sinkhorn(cost: Array<DoubleArray>, epsilon: Double, sourceWeights: DoubleArray? = null, targetWeights: DoubleArray? = null, maxIterations: Int = 10_000, tolerance: Double = 1e-9, checkEvery: Int = 20, initialF: DoubleArray? = null, initialG: DoubleArray? = null): TransportPlan Entropy-regularised optimal transport by log-domain Sinkhorn iteration; see the file overview.
fun sinkhorn(from: List<Vec2>, to: List<Vec2>, epsilon: Double, sourceWeights: DoubleArray? = null, targetWeights: DoubleArray? = null, maxIterations: Int = 10_000, tolerance: Double = 1e-9): TransportPlan sinkhorn between two 2D point sets with squared-distance cost, so epsilon is an area in the points' units. The sets may differ in size; the weights default to uniform.
fun sinkhornDivergence(from: List<Vec2>, to: List<Vec2>, epsilon: Double, sourceWeights: DoubleArray? = null, targetWeights: DoubleArray? = null, maxIterations: Int = 10_000, tolerance: Double = 1e-9): Double The Sinkhorn divergence OTε(μ, ν) − (OTε(μ, μ) + OTε(ν, ν)) / 2 between two weighted 2D point sets, with squared-distance cost: a distance-like measure of how different two distributions are that, unlike the regularised cost alone, is zero when they are the same and does not reward blurring. It costs three sinkhorn solves.
fun multiscaleSinkhorn(from: Array<DoubleArray>, to: Array<DoubleArray>, epsilon: Double, sourceWeights: DoubleArray? = null, targetWeights: DoubleArray? = null, minSize: Int = 64, maxIterationsPerLevel: Int = 5_000, tolerance: Double = 1e-9): TransportPlan sinkhorn for larger point sets, coarse to fine. Each set is reduced to a pyramid of farthest-point-sampled centres, each centre carrying the weight of the points nearest it, halving until about minSize remain; the coarsest pair is solved first, and each finer solve starts from the potentials of the one before, copied from each point's centre. The finest level is the full problem, so the answer is the same plan sinkhorn would reach; what changes is how many sweeps it takes to get there. Points are rows of coordinates of any dimension, with squared-distance cost.
fun squaredDistanceCost(from: List<Vec2>, to: List<Vec2>): Array<DoubleArray> The n × m matrix of squared distances from each of from to each of to.
fun squaredDistanceCost(from: Array<DoubleArray>, to: Array<DoubleArray>): Array<DoubleArray> squaredDistanceCost for points of any dimension, one row of coordinates per point.

HungarianAssignment

object HungarianAssignment

The Hungarian method for square cost matrices, O(n³). assignment is the public way in; this object is the one implementation that flow matching, the FLine trajectory matcher and the BSP-OT tests share.

Declaration Summary
fun solve(cost: Array<DoubleArray>): IntArray result[i](workspace.md#stdlib-kt--i) is the column assigned to row i; see assignment.

TransportPlan

class TransportPlan internal constructor(val plan: Array<DoubleArray>, val sourceWeights: DoubleArray, val targetWeights: DoubleArray, val f: DoubleArray, val g: DoubleArray, val epsilon: Double, val cost: Double, val objective: Double, val iterations: Int, val converged: Boolean, val rowError: Double, val columnError: Double)

The answer sinkhorn gives: a transport plan, the dual potentials that generate it, and how well it met its constraints.

Declaration Summary
val plan: Array<DoubleArray> plani` is how much of sourceigoes to targetj`. Rows sum to the source weights, columns to the target weights, and the whole plan to 1.
val sourceWeights: DoubleArray The source weights the plan was solved for, normalised to sum to 1.
val targetWeights: DoubleArray The target weights, normalised to sum to 1.
val f: DoubleArray The dual potential on each source; the plan is exp((f[i](workspace.md#stdlib-kt--i) + gj− costi) / ε).
val g: DoubleArray The dual potential on each target.
val epsilon: Double The regularisation the plan was solved at.
val cost: Double The transport cost Σ plani· costi``, without the entropy term.
val objective: Double The regularised objective cost + ε Σ π (ln π − 1) that was minimised.
val iterations: Int Row-then-column sweeps run.
val converged: Boolean Whether both marginal errors fell to the tolerance before the iteration limit.
val rowError: Double The largest difference between a row sum and its source weight.
val columnError: Double The largest difference between a column sum and its target weight.
val sources: Int The number of sources (rows).
val targets: Int The number of targets (columns).
fun barycentricMap(to: List<Vec2>): List<Vec2> Where each source goes on average: the plan-weighted mean of the targets it sends mass to. With a small ε this is close to the one target it is matched with; with a large one it is pulled toward the middle of everything it spreads over.
fun barycentricMap(to: Array<DoubleArray>): Array<DoubleArray> barycentricMap for points of any dimension: toj` is targetj`'s coordinates.
fun strongest(): IntArray For each source, the target that receives the most of it.
override fun toString(): String

voronoi.kt

Practical 3D Voronoi and approximate 3D multiplicatively weighted Voronoi ("CircularDirichlet") for Kotlin + Field linalg.

  • voronoi3d builds convex polyhedral cells by intersecting pairwise bisector half-spaces. Six clip-box planes are added to every cell, not only to the unbounded ones, so a cell always comes back as a bounded polyhedron; VoronoiCell3.bounded is how you tell whether the answer is the site's own cell or an artefact of the box. The box is the sites' bounding box grown on every side by clipPadding * scale + 1, where scale is the cloud's own diagonal (floored at 1).
  • CircularDirichlet3d.build extracts approximate support patches on bisector spheres / planes by clipping a sampled surface mesh with the weighted score predicate.

The weighted contourer is renderer-oriented: it prefers stable, wound boundary loops over exact symbolic patch topology.

How the unweighted clipper assigns a corner to a face

Combinatorially: a corner records the planes that produced it, and a face's polygon is exactly the corners recording that plane. Not by a distance test against a tolerance — a tolerance scaled off the point cloud lets faces near a near-cospherical site cluster claim corners that are not on them, and then the cells do not close, which is a T-junction and a hairline crack in any mesh built from the faces. The collectCorners KDoc in Voronoi3d has the detail.

Closure is not total, because the clipper works in Float: 4 of 465 bounded cells and 12 of 447 clipped cells fail the strict check that every edge is walked by exactly two faces in opposite directions. VoronoiFaceTopologyTest pins both counts with <=, so they can improve but cannot regress silently. Cell volumes tile the clip box to 1e-4 relative, and a cubic lattice of sites gives exact unit cubes.

src/jvmMain/kotlin/afield/geometry/spatial/voronoi.kt — read the source

Used in: Points into structure — Voronoi, Apollonius, contours and clusters, Meshes and 3D geometry

Top-level functions

Declaration Summary
fun voronoi3d(l: List<Vec3>): VoronoiLattice The 3D Voronoi diagram of l, with the default tolerance and a generous clip box.

Aabb3

data class Aabb3(val min: Vec3, val max: Vec3)

An axis-aligned box; by convention min is componentwise <= max. The clipper's own window.

Declaration Summary
val min: Vec3 The low corner, componentwise.
val max: Vec3 The high corner, componentwise.

Plane3

data class Plane3(val normal: Vec3, val d: Float)

The plane normal · x = d.

Declaration Summary
val normal: Vec3 The plane's normal, not normalized — for a bisector it is the raw difference of two sites.
val d: Float The offset: the plane is the points x with dot(normal, x) == d. Scaled by normal's length.

VoronoiFace

data class VoronoiFace(val neighborSite: Int?, val plane: Plane3, val polygon: List<Vec3>, val isClip: Boolean, val truncated: Boolean)

One face of a VoronoiCell3: the plane it lies in, and the cell's corners that lie on it.

Declaration Summary
val neighborSite: Int? The site across this face, or null when this is a clip-box face rather than a real bisector.
val plane: Plane3 The plane this face lies in: the perpendicular bisector of the two sites, or a clip-box side.
val polygon: List<Vec3> The face outline, wound counter-clockwise as seen from the +plane.normal side — its signed area along Plane3.normal is non-negative.
val isClip: Boolean true if this face is a side of the clip box rather than a bisector between two sites.
val truncated: Boolean true if any of this face's corners touches the clip box, so the face is cut by the window rather than closed by the sites.

VoronoiCell3

data class VoronoiCell3(val siteIndex: Int, val site: Vec3, val faces: List<VoronoiFace>, val bounded: Boolean)

The convex polyhedron of points nearer to site than to any other site, intersected with the lattice's clip box.

Declaration Summary
val siteIndex: Int Which input site this is the cell of, as an index into the list that was clustered.
val site: Vec3 The site itself.
val faces: List<VoronoiFace> The bounding faces, in no particular order. See VoronoiFace.
val bounded: Boolean true only if no face is a clip face and no face is VoronoiFace.truncated — that is, the cell is closed by its neighbours alone and the clip box did not participate.

VoronoiLattice

data class VoronoiLattice(val cells: List<VoronoiCell3>, val clipBounds: Aabb3)

One cell per input site, in input order, plus the box they were all clipped against.

Declaration Summary
val cells: List<VoronoiCell3> One cell per input site, in input order — so cells[i](workspace.md#stdlib-kt--i) is the cell of site i.
val clipBounds: Aabb3 The box every cell was clipped against. Its volume is what the cell volumes must sum to.
operator fun get(siteIndex: Int): VoronoiCell3 The cell of site siteIndex — the same thing as cellssiteIndex``, and as loud when out of range.

Voronoi3d

object Voronoi3d

The unweighted 3-D clipper, with its knobs exposed — voronoi3d is this with the defaults.

Declaration Summary
fun build(points: List<Vec3>, tolerance: Float = 1e-4f, clipPadding: Float = 8f): VoronoiLattice Build the Voronoi diagram of points, clipped to a box.

WeightedSite3

data class WeightedSite3(val position: Vec3, val weight: Float)

A site with an influence radius: distance to it is measured as |x − position| / weight.

Declaration Summary
val position: Vec3 Where the site is.
val weight: Float The divisor on distance: larger claims more space. Must be strictly positive — CircularDirichlet3d.build rejects zero and negative weights by index rather than producing nonsense.

SupportSurface3

sealed interface SupportSurface3

The surface a weighted bisector lives on: a sphere when the two weights differ, a plane when they are equal.

Declaration Summary
val samplePoint: Vec3 Any one point known to lie on the surface — the seed a contour walk starts from.

SupportSphere3

data class SupportSphere3(val center: Vec3, val radius: Float) : SupportSurface3

An Apollonius sphere: the locus where two differently-weighted sites are equidistant.

Declaration Summary
val center: Vec3 Centre of the sphere. Not either site's position.
val radius: Float Radius of the sphere, strictly positive — a degenerate solution is discarded rather than emitted.
override val samplePoint: Vec3 A point on the sphere, taken along +x from the centre. Arbitrary but deterministic.

SupportPlane3

data class SupportPlane3(val normal: Vec3, val d: Float) : SupportSurface3

The ordinary perpendicular bisector plane, which is what a weighted bisector degenerates to when the two weights are equal (within the build tolerance).

Declaration Summary
val normal: Vec3 The plane's normal, not normalized.
val d: Float The offset: the plane is the points x with dot(normal, x) == d.
override val samplePoint: Vec3 The point on the plane closest to the origin.

CircularDirichletPatch3

sealed interface CircularDirichletPatch3

One piece of one cell's boundary in the weighted diagram: the part of a bisector surface that is actually on the boundary once every other site has had its say.

Declaration Summary
val siteIndex: Int The cell this patch bounds.
val neighborSite: Int The site on the other side of it.
val support: SupportSurface3 The unbounded surface this patch was cut out of.
val truncated: Boolean true when the clip box cut this patch, so its extent is the window's and not the sites'.

SpherePatch3

data class SpherePatch3(override val siteIndex: Int, override val neighborSite: Int, override val support: SupportSphere3, val boundary: List<Vec3>, override val truncated: Boolean) : CircularDirichletPatch3

A patch of an Apollonius sphere.

Declaration Summary
override val siteIndex: Int The cell this patch bounds.
override val neighborSite: Int The site on the other side of it.
override val support: SupportSphere3 The sphere this patch lies on.
val boundary: List<Vec3> The patch outline as a wound loop of points on the sphere, or empty to mean the entire sphere rather than an empty patch (an empty patch is not emitted at all).
override val truncated: Boolean true when the clip box cut this patch, so its extent is the window's and not the sites'.

PlanePatch3

data class PlanePatch3(override val siteIndex: Int, override val neighborSite: Int, override val support: SupportPlane3, val polygon: List<Vec3>, override val truncated: Boolean) : CircularDirichletPatch3

A patch of an ordinary bisector plane, between two equally-weighted sites.

Declaration Summary
override val siteIndex: Int The cell this patch bounds.
override val neighborSite: Int The site on the other side of it.
override val support: SupportPlane3 The plane this patch lies on.
val polygon: List<Vec3> The patch outline as a wound loop of points on the plane. Never empty.
override val truncated: Boolean true when the clip box cut this patch, so its extent is the window's and not the sites'.

CircularDirichletCell3

data class CircularDirichletCell3(val siteIndex: Int, val patches: List<CircularDirichletPatch3>)

One weighted cell, as the collection of boundary patches that fence it in.

Declaration Summary
val siteIndex: Int Which weighted site this is the cell of.
val patches: List<CircularDirichletPatch3> The boundary pieces, sorted by CircularDirichletPatch3.neighborSite then by support size.

CircularDirichletDiagram3

data class CircularDirichletDiagram3(val cells: List<CircularDirichletCell3>, val clipBounds: Aabb3)

One weighted cell per input site, in input order, plus the box they were all clipped against.

Declaration Summary
val cells: List<CircularDirichletCell3> One cell per input site, in input order.
val clipBounds: Aabb3 The box the patches were clipped against, built the same way as VoronoiLattice.clipBounds.
operator fun get(siteIndex: Int): CircularDirichletCell3 The cell of site siteIndex.

CircularDirichlet3d

object CircularDirichlet3d

The multiplicatively weighted diagram, by sampling and contouring rather than by exact construction.

Declaration Summary
fun build(sites: List<WeightedSite3>, tolerance: Float = 1e-4f, clipPadding: Float = 8f, sphereSubdivisions: Int = 3, planeSteps: Int = 56): CircularDirichletDiagram3 Build the weighted diagram of sites.

Mapper.kt

Mapper: a graph that summarises the shape of a point cloud, in any number of dimensions.

Mapper.compute gives every point a lens value, cuts the range of the lens into overlapping bins, clusters the points within each bin, and joins clusters in neighbouring bins that share points. The MapperGraph that comes back has a MapperNode per cluster and a MapperEdge wherever two clusters share points, so a cloud with branches, loops or separate pieces produces a graph with branches, loops or separate pieces; MapperGraph.components and MapperGraph.loops count those directly.

The lens decides what the graph can see. Mapper.coordinate slices the cloud along one axis, Mapper.eccentricity measures how far each point is from the rest and so picks out the ends of branches whatever direction they run in, and Mapper.density measures how crowded a point's neighbourhood is. Any array with one value per point will do.

MapperGraph.toFLines draws the graph over the cloud's first two coordinates, each node sized by how many points it holds and coloured by its mean lens value; MapperGraph.pointsFLine draws the points themselves; and MapperGraph.network hands the graph on as a GeometryNetwork. The worked account is public/docs/mapper.md.

src/jvmMain/kotlin/afield/geometry/topology/mapper/Mapper.kt — read the source

Used in: Mapper graphs of point clouds

MapperNode

class MapperNode(val id: Int, val bin: Int, val members: IntArray, val centroid: DoubleArray, val lens: Double)

A node of a MapperGraph: one cluster of points within one bin of the lens. members are point indices; centroid their mean position (all MapperGraph.dimension coordinates) and lens their mean lens value.

Declaration Summary
override fun toString()

MapperEdge

data class MapperEdge(val a: Int, val b: Int, val shared: Int)

An edge of a MapperGraph: nodes a and b share shared points.

MapperGraph

class MapperGraph internal constructor(val dimension: Int, val points: DoubleArray, val lensValues: DoubleArray, val nodes: List<MapperNode>, val edges: List<MapperEdge>)

The Mapper graph of a point cloud: a cluster per node, an edge wherever two clusters share points. It is a coarse picture of the cloud's shape: branches, loops and separate pieces show up as branches, loops and separate pieces of the graph.

Declaration Summary
val dimension: Int
val points: DoubleArray
val lensValues: DoubleArray
val nodes: List<MapperNode>
val edges: List<MapperEdge>
val nodesOfPoint: List<IntArray> by lazy For each point, the nodes it belongs to (one, or two where bins overlap; none if it was noise).
fun components(): List<List<Int>> The graph's connected components, as lists of node ids.
fun toFLines(minSize: Double = 4.0, maxSize: Double = 14.0, colormap: (Double) -> Vec4 = ::lensColour): List<FLine> The graph drawn over the cloud's first two coordinates: each edge a line between its nodes' centroids, each node a dot sized by how many points it has (minSize to maxSize pixels) and coloured by its mean lens value through colormap (low to high).
fun pointsFLine(pointSize: Double = 2.0, colormap: (Double) -> Vec4 = ::lensColour): FLine The points as one pointed FLine (first two coordinates), each coloured by its lens value, so you can see what the lens measured.
fun network(): GeometryNetwork The graph as a GeometryNetwork: nodes at their centroids (channels size, lens), each edge a straight curve (attribute shared).
override fun toString()

Mapper

object Mapper

Mapper (Singh, Mémoli and Carlsson, 2007): a graph summarising the shape of a point cloud.

Declaration Summary
fun compute(points: DoubleArray, dimension: Int = 2, lens: DoubleArray, intervals: Int = 10, overlap: Double = 0.3, eps: Double? = null, minClusterSize: Int = 3): MapperGraph The Mapper graph of points (dimension coordinates per point) with one lens value per point.
fun compute(points: List<Vec2>, lens: DoubleArray, intervals: Int = 10, overlap: Double = 0.3, eps: Double? = null, minClusterSize: Int = 3): MapperGraph The Mapper graph of 2D points.
fun coordinate(points: DoubleArray, dimension: Int = 2, axis: Int = 0): DoubleArray Each point's first coordinate (axis 0), second (1), and so on: a lens that slices the cloud.
fun eccentricity(points: DoubleArray, dimension: Int = 2): DoubleArray Each point's mean distance to every other point: small in the middle of the cloud, large at its extremities. Picks out the ends of branches whatever their direction. O(n²).
fun density(points: DoubleArray, dimension: Int = 2, k: Int = 8): DoubleArray Each point's distance to its kth nearest neighbour, negated, so larger is more crowded: a lens that separates dense cores from sparse fringes. O(n²).
fun eccentricity(points: List<Vec2>)
fun density(points: List<Vec2>, k: Int = 8)

MorseSmale.kt

The Morse–Smale complex of a 2-D scalar field: where its peaks, pits and passes are, how they are joined, and which of them are significant.

MorseSmale is the entry point. It builds a MorseSmaleComplex once, from an image channel or a float grid, and the complex then answers for any persistence threshold without being rebuilt, which is what makes a persistence slider cheap. Each threshold gives a MorseSmaleResult: the surviving CriticalPoints, the separatrices between them as a GeometryNetwork, and the basin and peak segmentation as one id per pixel. The whole hierarchy is readable from the complex as PersistencePairs and Cancellations, and MorseSmaleTimings splits the build by stage.

Persistence is in the field's own units and removes features by significance, so it is a different thing from blurring the field or smoothing the drawn separatrices. What the threshold does to a picture, and what the complex is good for once you have it, are in public/docs/morse_smale.md; following the critical points through time or scale is afield.geometry.topology.tracking.

The complex is computed by the optional native library field_morse_smale (MorseSmaleLibrary), so it holds native memory and is closed by the box's next run.

src/jvmMain/kotlin/afield/geometry/topology/morsesmale/MorseSmale.kt — read the source

Used in: Morse–Smale complexes: the shape of a scalar field, Tracking features through time and scale

CriticalType

enum class CriticalType

What kind of critical point: a minimum is a vertex, a saddle an edge, a maximum a triangle of the grid.

Constants: MINIMUM, SADDLE, MAXIMUM.

CriticalPoint

data class CriticalPoint(val id: Int, val type: CriticalType, val value: Double, val x: Double, val y: Double, val cancelledAt: Double)

A critical point of the unsimplified complex.

Declaration Summary
val id: Int
val type: CriticalType
val value: Double
val x: Double
val y: Double
val cancelledAt: Double

PersistencePair

data class PersistencePair(val birth: Int, val death: Int?, val dimension: Int, val persistence: Double)

One pair of the persistence diagram. dimension 0 pairs a minimum (birth) with the saddle that merges its basin away (death); dimension 1 pairs a saddle with the maximum that fills its cycle. The global minimum's death is null and its persistence infinite.

Cancellation

data class Cancellation(val saddle: Int, val extremum: Int, val persistence: Double)

One step of the simplification hierarchy: saddle and extremum cancel at persistence.

MorseSmaleTimings

data class MorseSmaleTimings(val gradientMs: Double, val separatricesMs: Double, val hierarchyMs: Double, val persistenceMs: Double)

Wall-clock milliseconds spent building a complex, by stage.

MorseSmaleComplex

class MorseSmaleComplex internal constructor(val width: Int, val height: Int, private val values: FloatArray, handle: MemorySegment) : AutoCloseable

The discrete Morse–Smale complex of a scalar field, built once and simplified at any persistence threshold without rebuilding.

Declaration Summary
val width: Int
val height: Int
val criticalPoints: List<CriticalPoint> Every critical point of the unsimplified complex.
fun critical(id: Int): CriticalPoint? The critical point with cell id, or null.
val persistencePairs: List<PersistencePair> The persistence diagram, computed independently of the complex by union-find.
val cancellations: List<Cancellation> The simplification hierarchy in the order it is applied; persistence never decreases along it.
val timings
fun survivalCurve(): List<Pair<Double, Int>> How many critical points survive at each threshold where the count changes: (threshold, count).
fun sample(x: Double, y: Double): Double The field value at (x, y) in pixel units, bilinear, clamped at the edges.
fun simplify(persistence: Double = 0.0): MorseSmaleResult The complex after cancelling every saddle–extremum pair with persistence below persistence, in field units. 0 gives the unsimplified complex. Replays the stored hierarchy, so it is cheap to call repeatedly from a slider.
val isClosed: Boolean
override fun close()
override fun toString()

MorseSmaleResult

class MorseSmaleResult internal constructor(val complex: MorseSmaleComplex, val threshold: Double, view: MorseSmaleNative.View)

A Morse–Smale complex at one persistence threshold.

Declaration Summary
val complex: MorseSmaleComplex
val threshold: Double
val criticalPoints: List<CriticalPoint>
val basins: IntArray
val peaks: IntArray
val separatrixCount: Int How many separatrices survive; known without building network.
val network: GeometryNetwork by lazy Built on first access, since an unsimplified noisy field can have millions of separatrices and a slider that only reads criticalPoints or basins should not pay for them.
fun count(type: CriticalType)
val descending: GeometryNetwork Separatrices running down to minima.
val ascending: GeometryNetwork Separatrices running up to maxima (or out of the grid).
fun toFLines(smooth: Boolean = false, traceDescending: Boolean = true, traceAscending: Boolean = true, descendingColor: Vec4 = Vec4(0.15f, 0.35f, 0.9f, 0.9f), ascendingColor: Vec4 = Vec4(0.9f, 0.25f, 0.15f, 0.9f), thickness: Double = 1.5): List<FLine> Separatrices as FLines: descending ones in descendingColor, ascending ones in ascendingColor. With smooth the polylines become splines through the same vertices; the topology does not change. Each line keeps curveId, startNode, endNode, and geometryNetwork, which is the whole network even when one kind is left out.
fun criticalPointsFLine(pointSize: Double = 6.0): FLine The surviving critical points as one FLine of dots: minima blue, saddles green, maxima red.
fun segmentationImage(peaks: Boolean = false): Img The basins (or, with peaks true, the peaks) as an RGB image, one colour per region.
override fun toString()

MorseSmale

object MorseSmale

Morse–Smale complexes of 2D scalar fields: critical points, the separatrices between them, basin and peak segmentation, and persistence-based simplification. Needs the optional native library field_morse_smale (see MorseSmaleLibrary).

Declaration Summary
val isAvailable: Boolean
fun build(values: FloatArray, width: Int, height: Int, pixelStride: Int = 1, rowStride: Int = width * pixelStride, offset: Int = 0): MorseSmaleComplex Builds the complex of a field with width × height samples, where sample (x, y) is values[offset + y * rowStride + x * pixelStride]. Nothing is simplified yet: call MorseSmaleComplex.simplify.
fun build(img: Img, channel: Int = 0): MorseSmaleComplex Builds the complex of one channel of img, read in place.
fun compute(values: FloatArray, width: Int, height: Int, persistence: Double = 0.0): MorseSmaleResult Builds, simplifies at persistence, and frees the native complex.
fun compute(img: Img, persistence: Double = 0.0, channel: Int = 0): MorseSmaleResult Builds, simplifies at persistence, and frees the native complex.
fun buildAsync(img: Img, channel: Int = 0): CompletableFuture<MorseSmaleComplex> build on a worker thread, for runAsync. A complex built off the box's thread is not owned by the run, so close it when you are done:
fun computeAsync(img: Img, persistence: Double = 0.0, channel: Int = 0): CompletableFuture<MorseSmaleResult> compute on a worker thread.

MorseSmaleNative.kt

Finding field_morse_smale on this machine, and calling into it once found.

MorseSmaleLibrary is the half that answers without loading anything: the library's file name on this OS, the paths it is looked for in, whether one of them exists, and the Gradle task and per-OS shell commands that build it. NativeGeometry registers it as a NativeGeometryCapability, so the welcome page's Geometry extensions panel can report on the library whether or not it is installed. The path is resolved afresh on every read, so a library built during a live session is picked up by the next box run.

The other half, internal to the package, binds the C ABI declared in native/field-morse-smale/src/field_morse_smale.h with java.lang.foreign: one lazy downcall handle per fms_ entry point, and a 512-byte buffer that building a complex and simplifying one carry down and read a message out of when they hand back nothing. The counts and read-outs take no error buffer. The first symbol lookup checks fms_abi_version against the version this binding expects and refuses a library that disagrees, naming the task that rebuilds it.

Two kinds of native object pass through here, and only one of them is the caller's to free. Building a complex hands back an address that must be freed exactly once, and reading it copies the critical points, the cancellation order, the persistence pairs and the stage timings out into Kotlin arrays in one pass. Simplifying at a threshold builds a second native object, a view; that call copies the surviving nodes, the separatrix arcs with their traced paths, and the basin and peak segmentation out of it, then frees the view before returning.

MorseSmale is the API over it, and its only caller.

src/jvmMain/kotlin/afield/geometry/topology/morsesmale/MorseSmaleNative.kt — read the source

MorseSmaleLibrary

object MorseSmaleLibrary : NativeGeometryCapability

Where field_morse_smale is, and whether it is there. Field runs without it; describe says how to build it.

Declaration Summary
override val name
override val title
override val docsPage
override val buildTask
override val install: Map<String, List<String>>
val fileName: String
val searchedPaths: List<Path> Every path resolvedPath tries, in order.
val resolvedPath: Path? Not cached: a library built during a live session is picked up by the next box run.
override val isAvailable: Boolean
val HOW_TO_BUILD: String
override fun describe(): String

FeatureTracking.kt

Following the topological features of a 2-D field from one moment to the next, so that a peak keeps its identity while it moves, and comparing two fields by their topology alone.

A Morse–Smale complex says where the peaks, pits and passes of one field are; it says nothing about which peak of this frame is which peak of the last. That correspondence is an assignment problem, and FeatureMatching solves it exactly for each moment, with a cost per match and a fixed price for leaving a feature unmatched (MatchingWeights), so births and deaths are priced in the same optimisation as the matches. FeatureTracker drives it frame by frame and accumulates Tracks; ScaleSpace runs the same matching over a blur series instead of over time, where the scale a feature survives to is a measure of its size.

The input is a list of Features, which MorseSmaleResult.features reads off a Morse–Smale result and VectorFieldTopology supplies from a vector field's critical points, so the tracker itself is indifferent to where the features came from.

persistenceDiagramDistance answers the other question, about whole fields rather than single features: how far apart two persistence diagrams are, exactly for diagrams of up to a few thousand pairs and entropically for larger ones. Everything here is plain Kotlin and needs no native library, though its usual source does. The worked account is public/docs/topology_tracking.md.

src/jvmMain/kotlin/afield/geometry/topology/tracking/FeatureTracking.kt — read the source

Used in: Tracking features through time and scale, Vector field topology

Top-level functions

Declaration Summary
fun MorseSmaleResult.features(minPersistence: Double = 0.0, types: Set<CriticalType> = setOf(CriticalType.MINIMUM, CriticalType.MAXIMUM)): List<Feature> The features of a Morse–Smale result whose persistence is at least minPersistence. Each critical point gets the persistence of its pair in the persistence diagram; the global minimum, which has no pair, gets infinity.
fun persistenceDiagramDistance(a: List<Pair<Double, Double>>, b: List<Pair<Double, Double>>, p: Double = 2.0, epsilon: Double? = null): Double The p-Wasserstein distance between two persistence diagrams, each a list of (birth, death) pairs: the least total cost, over all matchings, of moving each pair of one onto a pair of the other or onto the diagonal, with \|Δbirth\|^p + \|Δdeath\|^p per move and the result's p-th root. Pairs with an infinite death are left out. Small distances mean two fields with similar topology.
fun MorseSmaleResult.persistenceDiagram(dimension: Int): List<Pair<Double, Double>> The persistence diagram of a Morse–Smale result as (birth value, death value) pairs, in dimension 0 or 1.

Feature

data class Feature(val type: CriticalType, val x: Double, val y: Double, val value: Double, val persistence: Double, val sourceId: Int = -1)

A topological feature at one moment: a minimum, saddle or maximum with its position, value and persistence (how much it stands out: the persistence of the persistence pair it belongs to). sourceId is its id in the source it came from, e.g. a Morse–Smale cell.

Declaration Summary
val type: CriticalType
val x: Double
val y: Double
val value: Double
val persistence: Double
val sourceId: Int

MatchingWeights

data class MatchingWeights(val distanceWeight: Double = 1.0, val valueWeight: Double = 1.0, val birthDistance: Double = 10.0, val maxDistance: Double = Double.POSITIVE_INFINITY)

Weights for matching features between two moments.

Declaration Summary
val distanceWeight: Double
val valueWeight: Double
val birthDistance: Double
val maxDistance: Double

FeatureMatching

object FeatureMatching

Optimal matching between two sets of features: pairs of indices, null for born or died.

Declaration Summary
fun match(before: List<Feature>, after: List<Feature>, weights: MatchingWeights = MatchingWeights()): List<Pair<Int?, Int?>> Matches before to after at least total cost (see MatchingWeights). Returns one entry per feature: (i, j) for a match, (i, null) for a feature that dies, (null, j) for one born.

Track

class Track internal constructor(val id: Int, val type: CriticalType)

One feature followed through time (or scale): its samples, in order, and when it was born and died.

Declaration Summary
val samples (time, feature) for every moment the feature was seen.
val born: Double
val last: Double
var died: Double? Set when the feature failed to match at a later moment; null while it is still alive.
val isAlive: Boolean
fun toFLine(): FLine The track's path as an FLine in the feature's own coordinates.
override fun toString()

FeatureTracker

class FeatureTracker(val weights: MatchingWeights = MatchingWeights())

Follows features from moment to moment, giving each a track that persists for as long as the feature can be matched, by FeatureMatching, to one at the next moment.

Declaration Summary
val tracks: List<Track> Every track so far, alive or dead.
var alive: List<Track> The tracks alive after the latest add, in the order of the features passed to it.
var born: List<Track> Births and deaths in the latest add.
var died: List<Track>
fun add(time: Double, features: List<Feature>, motion: ((x: Double, y: Double) -> Pair<Double, Double>)? = null): List<Track> Adds the features seen at time, continuing, starting or ending tracks. Returns the tracks alive now.
fun toFLines(thickness: Double = 1.5, includeDead: Boolean = true): List<FLine> Every track's path as an FLine, coloured by track id, the alive ones ending in a dot.
fun aliveFLine(pointSize: Double = 7.0): FLine The alive features as dots, coloured by track id.
fun network(threeD: Boolean = false): GeometryNetwork Every track as a curve of a GeometryNetwork, in (x, y, time) when threeD, else (x, y). Curve id = track id; samples time, value, persistence; attributes born, died (infinity while alive) and type (0 minimum, 1 saddle, 2 maximum).

ScaleSpace

object ScaleSpace

Features followed across scale: the image blurred at each σ in turn, its Morse–Smale complex computed and simplified at persistence, and its extrema tracked from each σ to the next. A feature that survives to large σ is large; one that dies early is fine detail.

Declaration Summary
class Result(val sigmas: List<Double>, val tracker: FeatureTracker, val complexes: List<MorseSmaleResult>)
fun track(image: Img, sigmas: List<Double>, persistence: Double = 0.02, maxDistance: Double = 12.0, channel: Int = 0, types: Set<CriticalType> = setOf(CriticalType.MINIMUM, CriticalType.MAXIMUM)): Result Tracks extrema of channel channel of image across sigmas (increasing). Features are kept if their persistence is at least persistence, and matched within maxDistance pixels.

ContourTrees.kt

Merge trees and contour trees of a scalar field on a regular grid.

ContourTrees.joinTree follows the components of the sublevel sets as the threshold rises, ContourTrees.splitTree does the same from the top down, and ContourTrees.contourTree follows every component of every level set, so joins and splits are both recorded. Each hands back a ScalarTree of TreeNodes typed by TreeNodeType and TreeArcs between them, and every arc owns the pixels of the level-set component it stands for, readable per pixel through ScalarTree.arcOfPixel.

ScalarTree.simplify is what makes a tree readable: it repeatedly removes leaf arcs whose value span is under a persistence threshold, so small bumps and dips go first. ScalarTree.plot draws the tree with height for value, ScalarTree.segmentationImage paints each pixel by the arc that owns it, and ScalarTree.network hands the tree on as a GeometryNetwork.

The field is read as the same piecewise-linear surface Field's Morse–Smale complexes use: the grid cut into triangles along one diagonal, with ties between equal samples broken by pixel index. The worked account is public/docs/contour_trees.md.

src/jvmMain/kotlin/afield/geometry/topology/trees/ContourTrees.kt — read the source

Used in: Merge trees and contour trees

TreeNodeType

enum class TreeNodeType

The kind of a tree node: where a component of the level set begins or ends (MINIMUM, MAXIMUM), where two components join or one splits (SADDLE), or where the tree is rooted (ROOT, the global extremum a merge tree ends at).

Constants: MINIMUM, MAXIMUM, SADDLE, ROOT.

TreeNode

data class TreeNode(val id: Int, val vertex: Int, val value: Double, val x: Double, val y: Double, val type: TreeNodeType)

A node of a ScalarTree: the grid vertex it sits on, its value and position (pixels).

TreeArc

data class TreeArc(val id: Int, val lower: Int, val upper: Int, val vertices: IntArray)

An arc of a ScalarTree from node lower to node upper (by value), with the grid vertices it owns: the regular vertices whose level-set component it represents.

Declaration Summary
override fun equals(other: Any?)
override fun hashCode()

ScalarTree

class ScalarTree internal constructor(val kind: String, val width: Int, val height: Int, val nodes: List<TreeNode>, val arcs: List<TreeArc>)

A merge tree or contour tree of a scalar field on a regular grid: critical nodes joined by arcs, each arc owning the pixels whose level-set component it stands for.

Declaration Summary
val kind: String
val width: Int
val height: Int
val nodes: List<TreeNode>
val arcs: List<TreeArc>
fun node(id: Int): TreeNode
val arcOfPixel: IntArray by lazy Per pixel, the id of the arc that owns it (nodes' own pixels belong to an arc ending there), or -1.
fun arcsAt(id: Int): List<TreeArc> The arcs at node id.
fun simplify(persistence: Double): ScalarTree The tree with every leaf arc shorter than persistence (its value span) removed, repeatedly, and nodes left with one arc above and one below merged away. The leaf's pixels go to the arc it joined. This is the tree's persistence simplification: small bumps and dips vanish first.
fun plot(width: Double = 400.0, height: Double = 240.0, margin: Double = 0.0, color: Vec4 = Vec4(0.9f, 0.85f, 0.75f, 1f)): List<FLine> The tree drawn the classic way, as a plot: each node at height = its value (scaled into height pixels, higher values up), leaves spread evenly along x in depth-first order and every other node above the mean of the leaves below it; arcs drawn as right-angled elbows. margin insets the whole plot from the edges of the width by height area.
fun segmentationImage(): Img Each arc's pixels in its own colour: the field divided into the regions the tree's arcs stand for.
fun network(): GeometryNetwork The tree as a GeometryNetwork in image space: nodes at their pixels (channels type, value), each arc a straight curve between its nodes (attributes lowerValue, upperValue, pixels).
override fun toString()

ContourTrees

object ContourTrees

Merge trees and contour trees of 2D scalar fields.

Declaration Summary
fun joinTree(field: Img, channel: Int = 0): ScalarTree The join tree: sublevel components born at minima, joining at saddles, rooted at the global maximum.
fun splitTree(field: Img, channel: Int = 0): ScalarTree The split tree: superlevel components born at maxima, joining at saddles, rooted at the global minimum.
fun contourTree(field: Img, channel: Int = 0): ScalarTree The contour tree, by Carr, Snoeyink and Axen's merge of the augmented join and split trees: leaves are peeled off one at a time, each giving one contour-tree arc, until one vertex is left. O(n log n) for the sort and close to linear after it.

VectorFieldTopology.kt

The topology of a 2-D flow field: where it stands still, and the curves that divide it up.

VectorFieldTopology.compute reads the flow either as two images, u and v in their first channel, or as one RGB image with u in channel 0 and v in channel 1. It finds the field's zeros as FlowCriticalPoints, classified by FlowCriticalType into sources, sinks, saddles, the two kinds of focus and centres, together with the Separatrix curves that leave each saddle along its eigen-directions. Those curves cut the frame into regions inside which every streamline starts at the same place and ends at the same place, so they summarise the whole field rather than sampling it.

What comes back is ready to draw: VectorFieldTopology.streamlines for a grid-seeded picture of the flow, VectorFieldTopology.toFLines for the separatrices and VectorFieldTopology.criticalPointsFLine for the points, coloured by type. VectorFieldTopology.features hands the critical points to afield.geometry.topology.tracking.FeatureTracker, so a flow that changes can be followed frame by frame, and VectorFieldTopology.network gives the same topology as a GeometryNetwork, which is what the other geometry modules read.

Everything here is Kotlin, with no optional native library behind it. A scalar field can be read the same way through VectorFieldTopology.gradientOf, which turns one into its gradient, or into the flow along its contours. The worked account is public/docs/vector_field_topology.md.

src/jvmMain/kotlin/afield/geometry/topology/vectorfield/VectorFieldTopology.kt — read the source

Used in: Vector field topology

FlowCriticalType

enum class FlowCriticalType

What a flow does near a critical point: flows out (SOURCE), in (SINK), spirals out or in (REPELLING_FOCUS, ATTRACTING_FOCUS), circles (CENTER), or comes in along one direction and leaves along another (SADDLE).

Constants: SOURCE, SINK, SADDLE, REPELLING_FOCUS, ATTRACTING_FOCUS, CENTER.

FlowCriticalPoint

data class FlowCriticalPoint(val id: Int, val x: Double, val y: Double, val type: FlowCriticalType, val jacobian: DoubleArray)

A zero of the flow, at (x, y) in pixels. jacobian is the flow's derivative there, row-major (du/dx, du/dy, dv/dx, dv/dy); divergence and curl are its trace and rotation, and strength sqrt(|det J|), how fast the flow moves away from the point (in units of the field per pixel).

Declaration Summary
val id: Int
val x: Double
val y: Double
val type: FlowCriticalType
val jacobian: DoubleArray
override fun equals(other: Any?)
override fun hashCode()

Separatrix

class Separatrix(val saddle: Int, val unstable: Boolean, val points: DoubleArray, val end: Int?)

A separatrix: the streamline that leaves a saddle along one of its four eigen-directions. An unstable one flows out of the saddle (forward in time); a stable one flows into it (traced backward). points are pixel positions starting at the saddle; end is the critical point it reached, or null if it left the frame, ran out of length, or stopped at a point weaker than minStrength.

Declaration Summary
fun toFLine(): FLine

VectorFieldTopology

class VectorFieldTopology internal constructor(val width: Int, val height: Int, val criticalPoints: List<FlowCriticalPoint>, val separatrices: List<Separatrix>, private val field: Field)

The topology of a 2D vector field: its critical points, classified, and the separatrices joining its saddles to the rest. The separatrices divide the frame into regions inside which all streamlines start at the same place and end at the same place.

Declaration Summary
val width: Int
val height: Int
val criticalPoints: List<FlowCriticalPoint>
val separatrices: List<Separatrix>
fun of(vararg types: FlowCriticalType) Critical points of the given types.
fun toFLines(): List<FLine> The separatrices as FLines, unstable in UNSTABLE colour, stable in STABLE.
fun criticalPointsFLine(size: Double = 6.0): FLine The critical points as one pointed FLine, each node coloured by type: sources and repelling foci red, sinks and attracting foci blue, saddles white, centres yellow.
fun streamline(x: Double, y: Double, length: Double = 200.0, step: Double = 0.5, minSpeed: Double = 0.0): FLine The streamline through (x, y), traced length pixels forward and backward (RK4, steps of step pixels, stopping at the frame's edge or where the flow is slower than minSpeed). Streamlines follow the flow's direction whatever its speed, so a minSpeed keeps them out of places where the flow is only noise.
fun streamlines(spacing: Double = 12.0, length: Double = 40.0, color: Vec4 = Vec4(0.6f, 0.6f, 0.6f, 0.5f), minSpeed: Double = 0.0): List<FLine> Streamlines seeded on a grid every spacing pixels, each traced length pixels both ways: a quick picture of the flow to draw the topology over. Seeds where the flow is slower than minSpeed are skipped, and lines stop there.
fun features(minStrength: Double = 0.0): List<Feature> The critical points as tracking Features, for afield.geometry.topology.tracking.FeatureTracker: sinks and attracting foci as MINIMUM, sources and repelling foci as MAXIMUM, saddles as SADDLE (centres are left out). value is the divergence and persistence the strength.
fun network(): GeometryNetwork The topology as a GeometryNetwork in pixel space: critical points as nodes (channels type, the FlowCriticalType ordinal, and strength), separatrices as curves from their saddle to the point they reach (attribute unstable, 1 or 0). A separatrix that leaves the frame ends at a node of type -1 at its last point.
override fun toString()

Polyvector.kt

Turning a raster line drawing into curves, by the polyvector-field method of Bessmeltsev and Solomon (2019).

The problem this solves is the junction: where two strokes cross, a tracer that follows ink alone has no way to tell which outgoing branch continues which incoming one. A PolyvectorField gives every ink pixel two unsigned directions rather than one, so a crossing is a pixel where both strokes are represented and each can be followed through it. Polyvector is the entry point: Polyvector.fitField solves for the field, PolyvectorField.trace reads curves off it as a PolyvectorDrawing, and Polyvector.vectorize does both for a dark-on-light scan.

Fitting the field is the expensive step and tracing is not, which is why the controls come in two parts: FrameFieldParameters for the field, TraceParameters for the trace. A drawing can be re-traced at several prune ratios without the field being solved again.

The input is evidence, 0 to 1, for how much each pixel is ink, not a picture: Polyvector.inkOf gets it from a dark-on-light drawing as 1 - luminance, and an edge or ridge detector's output can be passed to Polyvector.fitField instead. Coordinates throughout are pixels at pixel centres, x right and y down. The worked account, including which drawings the method suits, is public/docs/polyvector.md.

The solver is the optional native library field_polyvector (PolyvectorLibrary), so a field holds native memory and is closed by the box's next run.

src/jvmMain/kotlin/afield/geometry/vectorize/polyvector/Polyvector.kt — read the source

Used in: Vectorizing line drawings: polyvector fields

FrameFieldParameters

data class FrameFieldParameters(val inkThreshold: Double = 90.0 / 255.0, val regularizer: Double = 0.1, val smoothness: Double = 50.0, val repairPasses: Int = 3)

Controls for building the frame field. inkThreshold is the evidence level (0..1) above which a pixel counts as ink. regularizer pushes the two directions towards a right angle (larger gives sharper junctions, smaller follows narrow junctions more closely). smoothness trades fidelity to the image's local direction for smoothness; raise it only for very noisy input. repairPasses fill single-pixel holes in the ink mask. The defaults are the authors'.

Declaration Summary
val inkThreshold: Double
val regularizer: Double
val smoothness: Double
val repairPasses: Int

TraceParameters

data class TraceParameters(val pruneRatio: Double = 0.75, val maxLoopPixels: Int = 4, val simplifyTolerance: Double = 0.01, val smooth: Boolean = true)

Controls for tracing curves on a field. pruneRatio (0..1) is how readily short side branches are removed; maxLoopPixels is the largest hole, in background pixels, that is closed over rather than kept as a loop; simplifyTolerance simplifies the final polylines (pixels); smooth applies the authors' final smoothing. The defaults are the authors'.

Declaration Summary
val pruneRatio: Double
val maxLoopPixels: Int
val simplifyTolerance: Double
val smooth: Boolean

PolyvectorField

class PolyvectorField internal constructor(val width: Int, val height: Int, private val evidence: FloatArray, val parameters: FrameFieldParameters, handle: MemorySegment) : AutoCloseable

The polyvector field of a line drawing: at every ink pixel, two unsigned directions that the lines through that pixel follow. Where one stroke runs, the two directions are the stroke's direction and one across it; where strokes cross, they are the two strokes' directions. Building the field is the expensive step; trace reads curves off it, and can be called again with different TraceParameters without rebuilding.

Declaration Summary
val width: Int
val height: Int
val parameters: FrameFieldParameters
val mask: ByteArray 1 where the evidence counts as ink, 0 elsewhere; row-major.
val weight: FloatArray Per pixel, the authors' confidence that one direction dominates there (0..1); row-major.
val directions: FloatArray Per pixel, the two field directions as unit vectors (x0, y0, x1, y1); zero off the ink.
val singularities: List<Vec2> Points where the field cannot be continued smoothly, in pixels.
val componentCount: Int Connected pieces of ink; each is vectorized on its own.
val fieldMs: Double Milliseconds spent building the field.
fun evidenceAt(x: Double, y: Double): Double The evidence at (x, y) in pixels, bilinear, clamped.
fun directionFLines(step: Int = 4, length: Double = 3.0, color0: Vec4 = Vec4(0.95f, 0.45f, 0.1f, 1f), color1: Vec4 = Vec4(0.15f, 0.5f, 0.95f, 1f), thickness: Double = 0.8): List<FLine> The field drawn as small crosses, one every step pixels on the ink: each cross is the two directions there, length pixels long, the first in color0 and the second in color1, thickness wide.
fun singularitiesFLine(pointSize: Double = 6.0, color: Vec4 = Vec4(1f, 0.1f, 0.6f, 1f)): FLine The singular points as one FLine of dots.
fun trace(parameters: TraceParameters = TraceParameters()): PolyvectorDrawing Traces curves on this field. Repeatable with different parameters; the field is not rebuilt.
val isClosed: Boolean
override fun close()
override fun toString()

PolyvectorDrawing

class PolyvectorDrawing internal constructor(val field: PolyvectorField, val parameters: TraceParameters, streamlines: List<FloatArray>, curves: List<FloatArray>, val droppedPoints: Int, val traceMs: Double)

A vectorized drawing: the curves traced on a field, and the streamlines they were built from. Coordinates are pixels at pixel centres, x right, y down.

Declaration Summary
val field: PolyvectorField
val parameters: TraceParameters
val droppedPoints: Int Non-finite points the reference produced and that were removed by splitting curves there.
val traceMs: Double
val curves: List<FloatArray> The final curves, each as x, y floats.
val streamlines: List<FloatArray> Streamlines traced along the field before any topology processing, each as x, y floats.
fun network(junctionTolerance: Double = 0.5): GeometryNetwork The curves as a GeometryNetwork. A node is placed only where two or more curve ends coincide (within junctionTolerance pixels); other ends have none, and a curve that ends against another curve's side stays unjoined. Per-vertex sample evidence: the input's value under the curve. Curve attribute meanEvidence.
val meanStrokeWidth: Double by lazy Ink area divided by total curve length: the drawing's mean stroke width, in pixels.
fun coverage(radius: Double = meanStrokeWidth / 2 + 1.5): Double How well the curves explain the ink: the share of ink pixels within radius pixels of a curve. The default radius is half the meanStrokeWidth plus a pixel and a half, so a stroke's full width counts as explained. 1 means every inked pixel is accounted for; a dropped stroke shows as a lower number.
fun toFLines(smooth: Boolean = false, color: Vec4 = Vec4(0.85f, 0.15f, 0.1f, 1f), thickness: Double = 1.5): List<FLine> The curves as FLines, each carrying curveId, startNode, endNode and geometryNetwork.
fun streamlinesFLine(color: Vec4 = Vec4(0.2f, 0.5f, 0.9f, 0.35f), thickness: Double = 0.6): FLine The streamlines as one FLine, for seeing what the topology stage started from.
override fun toString()

Polyvector

object Polyvector

Vectorization of line drawings by polyvector fields (Bessmeltsev & Solomon 2019): turns a raster drawing into smooth curves that follow its strokes through junctions and crossings. Needs the optional native library field_polyvector (see PolyvectorLibrary).

Declaration Summary
val isAvailable: Boolean
fun fitField(evidence: FloatArray, width: Int, height: Int, parameters: FrameFieldParameters = FrameFieldParameters()): PolyvectorField Builds the field of evidence (row-major, width × height, 1 = ink).
fun fitField(evidence: Img, parameters: FrameFieldParameters = FrameFieldParameters()): PolyvectorField Builds the field of an evidence image (channel 0, 1 = ink).
fun inkOf(drawing: Img): Img The evidence of a dark-on-light drawing: 1 - luminance.
fun vectorize(drawing: Img, field: FrameFieldParameters = FrameFieldParameters(), trace: TraceParameters = TraceParameters()): PolyvectorDrawing Field and trace in one call, for a dark-on-light drawing.
fun vectorizeAsync(drawing: Img, field: FrameFieldParameters = FrameFieldParameters(), trace: TraceParameters = TraceParameters()): CompletableFuture<PolyvectorDrawing> vectorize on a worker thread, for runAsync.

PolyvectorNative.kt

Finding field_polyvector on this machine, and calling into it once found.

PolyvectorLibrary is the half that answers without loading anything: the library's file name on this OS, the paths it is looked for in, whether one of them exists, and the Gradle task and per-OS shell commands that build it. Its Windows list is empty, which is how a NativeGeometryCapability says the build does not cover that OS yet, so the welcome page's Geometry extensions panel offers no install route there. The path is resolved afresh on every read, so a library built during a live session is picked up by the next box run.

The other half, internal to the package, binds the C ABI declared in native/field-polyvector/src/field_polyvector.h with java.lang.foreign: one lazy downcall handle per fpv_ entry point, two parameter structs laid out by hand with their padding (one for the field fit, one for the trace, following the split in FrameFieldParameters and TraceParameters), and a 512-byte buffer that the field fit and the trace carry down and read a message out of when either reports failure. The read-outs take no error buffer. The first symbol lookup checks fpv_abi_version against the version this binding expects and refuses a library that disagrees, naming the task that rebuilds it.

Reading a result copies it out of native memory into Kotlin arrays: the ink mask, the per-pixel weight, the two directions and the singularities as one snapshot of the field, the streamlines and the final curves as polylines, and the field and trace milliseconds the library times for itself.

Polyvector is the API over it, and its only caller.

src/jvmMain/kotlin/afield/geometry/vectorize/polyvector/PolyvectorNative.kt — read the source

PolyvectorLibrary

object PolyvectorLibrary : NativeGeometryCapability

Where field_polyvector is, and whether it is there. Field runs without it; describe says how to build it.

Declaration Summary
override val name
override val title
override val docsPage
override val buildTask
override val install: Map<String, List<String>>
val fileName: String
val searchedPaths: List<Path> Every path resolvedPath tries, in order.
val resolvedPath: Path? Not cached: a library built during a live session is picked up by the next box run.
override val isAvailable: Boolean
val HOW_TO_BUILD: String
override fun describe(): String

PolyvectorRequiredGraph.kt

Polyvector tracing under a graph the result has to reproduce: a Polyvector.vectorize overload that takes the curves the trace must keep, and a RequiredGraphDrawing saying which of them it kept.

The required graph is evidence rather than a hard constraint. Its curves are drawn into the field the tracer fits, so strokes follow them; traced ends landing within the snap tolerance of a required node are then moved onto that node and take its id, and a required node no traced end reached adopts a nearby traced junction instead. A connection can still be lost, which is what RequiredGraphDrawing.missingCurves and RequiredGraphDrawing.missingNodes report and RequiredGraphDrawing.satisfied answers in one call.

The graph usually comes from another reading of the same image, a Morse–Smale complex's network say, so that the traced curves share its nodes and their ids.

src/jvmMain/kotlin/afield/geometry/vectorize/polyvector/PolyvectorRequiredGraph.kt — read the source

Top-level functions

Declaration Summary
fun Polyvector.vectorize(drawing: Img, required: GeometryNetwork, seed: Double = 0.6, snapTolerance: Double = 3.0, reach: Double = 12.0, field: FrameFieldParameters = FrameFieldParameters(), trace: TraceParameters = TraceParameters()): RequiredGraphDrawing Polyvector.vectorize for a drawing whose connectivity must agree with required, a 2-D GeometryNetwork in the drawing's pixel coordinates (a Morse–Smale complex's network, say).

RequiredGraphDrawing

class RequiredGraphDrawing internal constructor(val drawing: PolyvectorDrawing, val required: GeometryNetwork, val network: GeometryNetwork, val missingCurves: List<Int>, val missingNodes: List<Int>)

A PolyvectorDrawing traced under a required graph, and how far its connectivity keeps that graph's. network is the traced curves with every end within the snap tolerance of a required node moved onto that node and given its id; other shared ends become new nodes, numbered after the required ones. A required curve between nodes a and b is kept when some chain of traced curves joins a to b without passing through another required node.

Declaration Summary
val drawing: PolyvectorDrawing
val required: GeometryNetwork
val network: GeometryNetwork
val missingCurves: List<Int> Ids of the required curves (between two nodes) that no chain of traced curves reproduces.
val missingNodes: List<Int> Ids of required nodes (those a required curve ends at) that no traced curve end reached.
val satisfied: Boolean
override fun toString()

Colormaps.kt

The colour vocabulary the visualization helpers share: Color4, a [0,1]-component colour value with no dependencies, and Colormaps, eight named ramps that can be sampled at any parameter.

A ramp here is a short table of evenly-spaced control points rather than a 256-entry lookup table (nine colours for each of the matplotlib maps, seventeen for turbo, three for the house cyan→orange), and Colormaps.sampleStops interpolates linearly between them. A caller that colours one pixel at a time builds its own table from that: showHeatmap quantises the ramp into 1024 entries once per image rather than interpolating per pixel.

Names are matched after trimming and lowercasing, gray, greys and cyan-orange are aliases, and an unknown name raises with Colormaps.names in the message rather than falling back to a default. RectSL's colormap and cmap words, showHeatmap and the imaging visualizers all take that same vocabulary, so a ramp named in one place is the same ramp in another.

src/jvmMain/kotlin/afield/graphics/color/Colormaps.kt — read the source

Used in: Imaging and computer vision

Color4

data class Color4(val r: Double, val g: Double, val b: Double, val a: Double = 1.0)

A colour with [0,1] components. A small, dependency-free colour value shared across the visualization helpers (showHeatmap, Colormaps) and the RectSL data-viz words.

Declaration Summary
val r: Double
val g: Double
val b: Double
val a: Double
fun withAlpha(alpha: Double): Color4
fun alphaTimes(alpha: Double): Color4
fun mix(other: Color4, t: Double): Color4

Colormaps

object Colormaps

Perceptually-uniform colormaps as named colour ramps — matplotlib's viridis / magma / plasma / inferno, Google's turbo, Kenneth Moreland's coolwarm diverging map, and a plain grayscale. Each map is a table of evenly-spaced RGB control points taken from the published data (the viridisLite 9-class swatches for the matplotlib family, the turbo_srgb_floats LUT sampled every 16th entry, Moreland's cool→warm table); sampleStops interpolates linearly between them. This is reusable domain code — the single source of truth for the showHeatmap visualizer and RectSL's colormap/cmap, bars, heat, and scatter words, and directly callable from Kotlin.

Declaration Summary
val viridis: List<Color4> matplotlib's default: dark purple through blue and green to yellow, nine stops from the viridisLite 9-class swatches. The safe first choice for a quantity with no natural midpoint.
val magma: List<Color4> Near-black through purple and red to pale cream, nine stops. Darker at the low end than viridis, which suits a heatmap whose background should read as empty.
val inferno: List<Color4> magma's hotter sibling: the same near-black start, but through crimson and orange to pale yellow, nine stops.
val plasma: List<Color4> Deep blue through magenta and orange to yellow, nine stops. Begins at a saturated blue rather than near-black, so the low end stays visible against a dark page.
val turbo: List<Color4> Google's turbo, the rainbow redrawn to run smoothly: dark blue, cyan, green, yellow, orange, dark red, seventeen stops taken from turbo_srgb_floats every sixteenth entry plus the last. More steps are told apart than in a perceptually uniform ramp, at the cost of implying boundaries the data may not have.
val coolwarm: List<Color4> Kenneth Moreland's cool-to-warm: blue through light grey to red, nine stops sampled every eighth of the way along his table. The one to reach for when zero means something — a residual, a difference of two runs — since the middle of the ramp is the neutral grey, so map the data to put zero there.
val grayscale: List<Color4> Black to white, two stops, so a sample is the parameter itself in all three channels. Matched by the names gray and greys as well.
val cyanorange: List<Color4> Field's house ramp: near-black, cyan through the middle, orange at the peaks, three stops. The same cyan and orange the signal plots use, and the default for the spectral heatmaps in the documentation. Also matched as cyan-orange.
val names: List<String> The eight canonical names, in the order above, as stops quotes them when it is handed one it does not know. The three aliases gray, greys and cyan-orange are accepted by stops but are not among them.
fun stops(name: String): List<Color4>
fun sample(name: String, t: Double): Color4 Look up colormap name at parameter t in [0,1] (clamped).
fun sampleStops(stops: List<Color4>, t: Double): Color4 Linearly interpolate an evenly-spaced list of control points at t in [0,1] (clamped).

colors.kt

Colour constructors for the stdlib. Everything here returns a Vec4 of sRGB-encoded components clamped to [0,1] with alpha last, so the results drop straight into FLine colours and uniforms.

rgb / rgba accept either 0..1 or 0..255 components — the scale is inferred per call from whether any component exceeds 1 — and rgb255 forces the 0..255 reading. hsl is HSL with the usual cube geometry, but the result is encoded through the sRGB transfer function on the way out, so it is not interchangeable with CSS hsl() — see hsl for what that costs. okhsl and okhsv are the Oklab-based equivalents; of the two only okhsl holds perceived lightness steady across hue, because okhsv scales v against the per-hue gamut cusp exactly as HSV always has. Hue may be given as a fraction of a turn or in degrees (magnitude above 1 is divided by 360) and is wrapped either way.

For sets: hueSetHsl, hueSetOkhsl and hueSetOkhsv (aliased hslSet, okhslSet, okhsvSet) space equally-separated hues around the wheel at a fixed saturation and lightness. paletteForBackground starts from the same okhsl ring but then walks each entry's lightness up or down until it clears a minimum WCAG contrast ratio against the supplied background, returning the highest-contrast candidate it found when it cannot reach the target.

src/jvmMain/kotlin/afield/graphics/color/colors.kt — read the source

Used in: Colors to FLine examples, Visualizers, Displays, and Little Instruments

Top-level functions

Declaration Summary
fun rgb(r: Number, g: Number, b: Number, a: Number = 1f): Vec4 A colour from sRGB components, on whichever of the two usual scales you happened to type.
fun rgba(r: Number, g: Number, b: Number, a: Number = 1f): Vec4 rgb under the name that reads better when you are passing alpha explicitly. Identical.
fun rgb255(r: Number, g: Number, b: Number, a: Number = 255f): Vec4 A colour from 0..255 sRGB components, with no scale guessing — the unambiguous form of rgb.
fun hsl(h: Number, s: Number, l: Number, a: Number = 1f): Vec4 A colour from classic hue / saturation / lightness.
fun hsla(h: Number, s: Number, l: Number, a: Number = 1f): Vec4 hsl under the name that reads better when you are passing alpha explicitly. Identical.
fun cssHsla(h: Number, s: Number, l: Number, a: Number = 1f): Vec4 CSS's hsla(): the colour a browser draws for hsla(h, s%, l%, a).
fun okhsl(h: Number, s: Number, l: Number, a: Number = 1f): Vec4 A colour from Oklab-based hue / saturation / lightness — HSL with the perceptual kinks taken out.
fun okhsv(h: Number, s: Number, v: Number, a: Number = 1f): Vec4 A colour from Oklab-based hue / saturation / value — the HSV-shaped sibling of okhsl.
fun hueSetHsl(count: Int, saturation: Number, lightness: Number, alpha: Number = 1f, startHue: Number = 0f): List<Vec4> count colours whose hues are spaced evenly right round the wheel, all at one HSL saturation and lightness — the one-liner for "give me n distinguishable colours".
fun hueSetOkhsl(count: Int, saturation: Number, lightness: Number, alpha: Number = 1f, startHue: Number = 0f): List<Vec4> count evenly spaced hues at one okhsl saturation and lightness — the palette generator to reach for first.
fun hueSetOkhsv(count: Int, saturation: Number, value: Number, alpha: Number = 1f, startHue: Number = 0f): List<Vec4> count evenly spaced hues at one okhsv saturation and value — hueSetOkhsl for the cases where you want the vivid end of the gamut rather than a fixed lightness.
fun hslSet(count: Int, saturation: Number, lightness: Number, alpha: Number = 1f, startHue: Number = 0f): List<Vec4> Short alias for hueSetHsl.
fun okhslSet(count: Int, saturation: Number, lightness: Number, alpha: Number = 1f, startHue: Number = 0f): List<Vec4> Short alias for hueSetOkhsl.
fun okhsvSet(count: Int, saturation: Number, value: Number, alpha: Number = 1f, startHue: Number = 0f): List<Vec4> Short alias for hueSetOkhsv.
fun paletteForBackground(background: Vec4, count: Int, saturation: Number = 0.72f, lightness: Number = 0.62f, alpha: Number = 1f, startHue: Number = 0f, minContrast: Number = 3f, maxLightnessShift: Number = 0.30f, lightnessSteps: Int = 24): List<Vec4> count evenly spaced okhsl hues, each nudged in lightness until it is actually legible against background.

pointing.kt

Turning a place a person is looking at back into a place in the world.

This file does one step of that: pixel → ray, the unprojection that gets you from a mouse position, a controller pose or a headset's aim to a line in the scene. PickScene is the next step, ray → named part, and Pointer the step before, person → pixel-or-pose. Once you have the ray, MeshBvh.raycast, intersectRayTriangle and intersectRayPlane answer what it hits.

The two camera models

Field has two unrelated camera models, and confusing them is the main hazard here:

  • MeshView — the orbital camera behind TriMesh.renderPng, Field's CPU rasterizer. Yaw, pitch, a distance in multiples of the mesh's own bounding-sphere radius, and a field of view. This is the one that makes pixels with no GPU, no window and no AWT, so it is the one a headless test or a docs capture can prove things with. MeshView.rayCamera inverts it, and does so by calling straight into VisionGeometryPreview.ViewFrame — the same object the rasterizer projects with, so the unprojection is the renderer's inverse by construction rather than by transcription.
  • afield.graphics.vulkan.util.Camera — the live Vulkan scene's camera: a lookAt model-view and a afield.graphics.vulkan.util.SuperProjection, which can be a perspective frustum, an off-centre one, an orthographic prism, or a Lie-group interpolation between perspective and ortho. There is no closed-form frustum to re-derive for that last case, so rayCameraFor handles all of them the same way: it inverts the clip matrix numerically, in double precision.

Conventions

  • Pixel coordinates run from the top-left of the frame, y downward, which is what GLFW's cursor reports (Window.cursor is glfwGetCursorPos) and what an image's rows are indexed by. Pixel centres are at half-integers: pixel (0, 0) covers [0,1) x [0,1) and its centre is (0.5, 0.5). All the rayThroughPixel overloads take the pixel index and aim at its centre.
  • Field's clip space is Vulkan's depth range, z in 0..1, and y is up in NDC despite Vulkan's default being down — afield.graphics.vulkan.Renderer flips it with a negative-height viewport (setViewportAndScissor sets y = rect.y + rect.h and height = -rect.h), precisely so that the familiar GL-style projection matrices keep working. So NDC y = +1 is the top row.
  • A ray's direction is unit length, so RayCamera hit distances are in world units and comparable between pointers.

Typical use

val mesh = icosphereMesh(2)
val view = MeshView(yawDegrees = 30.0, pitchDegrees = 20.0)
val camera = view.rayCamera(mesh)
val hit = mesh.bvh().raycast(camera.rayThroughPixel(360, 270))
println(hit)                                  // the triangle under the middle of the picture

src/jvmMain/kotlin/afield/graphics/interaction/pointing.kt — read the source

Top-level functions

Declaration Summary
fun MeshView.rayCamera(mesh: TriMesh, caption: String? = null, title: String? = null): MeshViewRayCamera The camera TriMesh.renderPng would use for mesh under this view — inverted, so its pixels map back into the world.
fun rayCameraFor(projection: Matrix4f, modelView: Matrix4f, pixelWidth: Int, pixelHeight: Int): ClipMatrixRayCamera A RayCamera over a projection and a model-view, in the order a vertex shader applies them.
fun afield.graphics.vulkan.util.Camera.rayCamera(pixelWidth: Int = renderer.window.width, pixelHeight: Int = renderer.window.height): ClipMatrixRayCamera This live scene camera, inverted — so a mouse position in the window becomes a ray in the scene.

Ray

class Ray(val originX: Double, val originY: Double, val originZ: Double, dirX: Double, dirY: Double, dirZ: Double)

A half-line in world space: where it starts and, in unit length, where it goes.

Declaration Summary
val originX: Double Where the ray starts.
val originY: Double
val originZ: Double
val dirX: Double Unit-length direction.
val dirY: Double
val dirZ: Double
val origin: Vec3 A fresh Vec3 at the ray's start — mutating it does not touch the ray.
val direction: Vec3 A fresh unit-length Vec3 along the ray.
fun at(t: Double): DoubleArray The point t world units along the ray, in double precision.
fun pointAt(t: Double): Vec3 at, as a fresh Vec3.
override fun toString(): String

PixelAt

data class PixelAt(val x: Double, val y: Double, val depth: Double)

Where a world point landed: a pixel, and how far in front of the eye it was.

Declaration Summary
val x: Double Column, in pixels from the left edge. Not rounded — a point lands between pixels.
val y: Double Row, in pixels from the top edge.
val depth: Double Depth. Its units are the camera's own business — MeshViewRayCamera reports view-space z in world units, ClipMatrixRayCamera reports normalized device z in 0..1 — so compare two depths from the same camera and nothing else.
fun isInside(width: Int, height: Int): Boolean True when this landed inside a width x height frame.

RayCamera

interface RayCamera

A camera you can run backwards: pixel to ray, and world point back to pixel.

Declaration Summary
val pixelWidth: Int Frame width in pixels.
val pixelHeight: Int Frame height in pixels.
fun rayThrough(x: Double, y: Double): Ray The ray leaving the eye through the point (x, y) in pixel coordinates, y downward.
fun rayThroughPixel(x: Int, y: Int): Ray rayThrough the centre of pixel (x, y) — i.e. through (x + 0.5, y + 0.5).
fun pixelOf(x: Double, y: Double, z: Double): PixelAt? Where a world point lands, or null when it is at or behind the eye and so has no pixel.
fun pixelOf(point: Vec3): PixelAt? pixelOf a Vec3.
val isNonDegenerate: Boolean Whether this camera actually spreads the frame out — the premise every round-trip test needs.
fun nonDegeneracyMargin(): Double The smallest angle, in radians, between the rays through the frame's four quarter points.

MeshViewRayCamera

class MeshViewRayCamera(val frame: VisionGeometryPreview.ViewFrame) : RayCamera

MeshView's camera, inverted — the one that maps pixels of a TriMesh.renderPng back into the world.

Declaration Summary
val frame: VisionGeometryPreview.ViewFrame The rasterizer's projection, both ways.
override val pixelWidth: Int
override val pixelHeight: Int
val eye: Vec3 Where the eye stands, as a fresh Vec3.
override fun rayThrough(x: Double, y: Double): Ray
override fun rayThroughPixel(x: Int, y: Int): Ray
override fun pixelOf(x: Double, y: Double, z: Double): PixelAt?

ClipMatrixRayCamera

class ClipMatrixRayCamera(clip: Matrix4f, override val pixelWidth: Int, override val pixelHeight: Int) : RayCamera

Any projection at all, inverted numerically: a clip matrix, a frame size, and double-precision Gauss-Jordan.

Declaration Summary
override val pixelWidth: Int
override val pixelHeight: Int
override fun rayThrough(x: Double, y: Double): Ray
override fun pixelOf(x: Double, y: Double, z: Double): PixelAt?

pointing_pick.kt

A scene you can point at, and a hit record that names the part you touched rather than the triangle.

Why a triangle index is not an answer

MeshBvh.raycast returns a RayHit, and RayHit.triangle is an index into an TriMesh's indices array. That is the right thing for a physics query and the wrong thing for a person: a human pointing at a house wants to hear "the east wall", not "triangle 4471", and a triangle index changes every time the geometry is rebuilt, so it cannot be written down, logged, compared between two runs or used as a selection.

So a pickable carries PickParts — a per-triangle part id with names — and a PickHit reports the part. Three things then become possible that were not: naming the thing you touched, keeping a selection across a re-render, and asking whether two pointers found the same part.

Where a part id comes from

In descending order of how much it is worth trusting:

  1. The geometry already knows. PickParts.fromTriangulated reads TriangulatedGeometry.sourceFace — the polytope layer has recorded, for every triangle it emits, the polygonal face it was cut from — well covered by the polytope tests and, until now, read by nothing in jvmMain outside polytope itself. This is the only source here where the id means something outside the mesh: it is an afield.geometry.polytope.ElementId in the polytope's own topology, so it survives re-triangulation with a different triangulator and can be handed back to the polytope API.
  2. The author said so. PickParts.of takes the array, for a builder that knows which triangles it just appended — see TriMeshBuilder.groups, which records it for you.
  3. The topology can guess. PickParts.byComponent splits on connected components. Cheap and cheap, and exactly as good as the mesh's connectivity: a house whose walls all share vertices is one component and therefore one part, which is a real limitation and not a bug here.

What happens when the geometry is rebuilt

PickParts is keyed by triangle index and valid only for the mesh it was built from. Pickable checks the triangle count on construction, which catches the gross case; it cannot catch a mesh of the same size with different triangles, and does not pretend to. If you rebuild geometry every frame, rebuild the PickParts with it and carry the part id — never the triangle — across the rebuild. That is the whole reason ids exist.

Typical use

val floor = planeMesh(width = 4.0, height = 4.0)
val scene = pickScene {
    add("floor", floor, parts = PickParts.single(floor, "floor"))
    add("lamp", icosphereMesh(2), at = Matrix4f().translation(0f, 1.5f, 0f))
}
val camera = MeshView().rayCamera(scene.mergedMesh())
val hit = scene.pick(camera.rayThroughPixel(360, 270))
println(hit?.describe())      // "lamp / lamp  tri 812  1.8300 away  at (0.3100, 1.7200, 0.4400)"

src/jvmMain/kotlin/afield/graphics/interaction/pointing_pick.kt — read the source

Top-level functions

Declaration Summary
fun TriMeshBuilder.groups(): PickParts A PickParts recording the groups this builder was told about — the "the author said so" source, and the one to reach for when you are assembling a model out of named pieces.
fun pickScene(block: PickSceneBuilder.() -> Unit): PickScene Build a PickScene. See PickSceneBuilder.add.

PickParts

class PickParts private constructor(private val storedTriangleParts: IntArray, val names: List<String>, val externalIds: List<Int?>)

Which named part each of a mesh's triangles belongs to — the "stable picking id through triangles" a pick record needs in order to say something a person can act on.

Declaration Summary
val names: List<String> Part names, indexed by part id.
val externalIds: List<Int?> For fromTriangulated, the part's id in the geometry's own world — a polytope afield.geometry.polytope.ElementId's value. null for parts that have no such identity.
val triangleCount: Int Triangles this covers. Must equal the mesh's triangleCount.
val partCount: Int How many named parts there are.
val triangleParts: IntArray Per-triangle part indices, as a fresh array.
fun partOf(triangle: Int): Int Which part triangle triangle belongs to, or UNASSIGNED.
fun nameOfTriangle(triangle: Int): String The name of the part triangle triangle belongs to, or UNASSIGNED_NAME.
fun nameOfPart(part: Int): String The name of part part, or UNASSIGNED_NAME when it is out of range.
fun externalIdOfPart(part: Int): Int? Part part's identity in the geometry it came from, or null. See externalIds.
fun trianglesOfPart(part: Int): IntArray Every triangle belonging to part part. Linear in the triangle count.
fun triangleCounts(): IntArray How many triangles each part got, indexed by part. Useful in a caption.
override fun toString(): String

Pickable

class Pickable(val name: String, val mesh: TriMesh, val parts: PickParts = PickParts.single(mesh, name), val at: Matrix4f? = null, val enabled: Boolean = true, leafSize: Int = 8)

One thing in a scene that a ray can hit: CPU geometry, a name, a part assignment, and where it stands.

Declaration Summary
val name: String What to call this when a pick reports it.
val mesh: TriMesh The geometry. Captured by reference; nothing here mutates it.
val parts: PickParts Which part each triangle belongs to. Defaults to one part named after the object.
val at: Matrix4f? Model-to-world. null means the mesh is already in world coordinates, and skips the maths.
val enabled: Boolean Set false to leave it in the scene but stop it answering — a "look through this" flag.
val bvh: MeshBvh The BVH over mesh, in model space.
override fun toString(): String

PickHit

class PickHit(val pickable: Pickable, val objectName: String, val part: Int, val partName: String, val externalId: Int?, val triangle: Int, val distance: Double, val point: Vec3, val normal: Vec3, val u: Double, val v: Double, val localPoint: Vec3)

What a pointer touched: which object, which named part, and the geometry of the contact.

Declaration Summary
val pickable: Pickable The thing that was hit.
val objectName: String Pickable.name, hoisted so a hit can be logged without holding the geometry.
val part: Int Part id within Pickable.parts, or PickParts.UNASSIGNED.
val partName: String The part's name.
val externalId: Int? The part's identity in the geometry it came from — a polytope face's ElementId value — or null. This is the id worth writing down; triangle is not.
val triangle: Int Index of the triangle hit, in Pickable.mesh. Changes whenever the geometry is rebuilt.
val distance: Double Distance from the ray's origin, in world units. See Pickable's note on scale.
val point: Vec3 The contact point, in world space.
val normal: Vec3 The hit triangle's geometric normal in world space, unit length, not flipped toward the ray.
val u: Double Barycentric weight on the triangle's second corner. u + v <= 1.
val v: Double Barycentric weight on the triangle's third corner. u + v <= 1.
val localPoint: Vec3 The contact point in the pickable's own model space — where its geometry thinks it is.
fun isFrontFacing(ray: Ray): Boolean True when the normal points back toward where the ray came from.
fun describe(): String One line a person can read. The println of a pick.
override fun toString(): String

PickScene

class PickScene(pickables: List<Pickable> = emptyList())

Several Pickables, and nearest-hit resolution across them.

Declaration Summary
val pickables: List<Pickable> Everything in the scene, in the order it was added.
operator fun get(name: String): Pickable? The pickable called name, or null. First match wins if two share a name.
operator fun plus(pickable: Pickable): PickScene A scene with pickable added. PickScene is immutable; this returns a new one.
val triangleCount: Int Total triangles across every pickable, enabled or not.
fun pick(ray: Ray, maxDistance: Double = Double.MAX_VALUE, cullBackFaces: Boolean = false): PickHit? The nearest thing ray hits, or null.
fun pickAll(ray: Ray, maxDistance: Double = Double.MAX_VALUE, cullBackFaces: Boolean = false): List<PickHit> Every hit along ray, nearest first — one per pickable, its own nearest.
fun mergedMesh(): TriMesh Every pickable's geometry, transformed into world space and concatenated — for framing a camera on the whole scene, and for rendering it in one call.
fun mergedParts(): PickParts mergedMesh's part assignment, named "object / part" so a merged hit is still legible.

PickSceneBuilder

class PickSceneBuilder

Collects Pickables for pickScene.

Declaration Summary
fun add(name: String, mesh: TriMesh, parts: PickParts = PickParts.single(mesh, name), at: Matrix4f? = null, enabled: Boolean = true): Pickable Add something pointable.
fun add(pickable: Pickable): Pickable Add an already-built Pickable.

pointing_pointer.kt

A person pointing — mouse, XR controller or pinched hand — behind one interface, and the hover / press / drag / release machine that turns a stream of samples into an interaction.

A pointer is three things

A ray, a select value in 0..1, and whether it exists at all (PointerState). That is enough to unify every pointing device Field can see:

  • A mouse has a ray only because a camera can be inverted (mousePointer), a select value that is 0 or 1, and is always active while the window has a cursor.
  • An XR controller has a pose and a trigger, and reports active when the runtime is tracking it (controllerPointer).
  • A pinched hand has 26 joints and a geometric pinch strength, read off afield.graphics.vulkan.targets.OpenXRHmdTarget's handState. Its select value is continuous and noisy, which is what the second half of this file is about (pinchPointer).

Hysteresis, in two places

Feed raw nearest-hit results to a UI and it flickers. There are two flickers here and PointerInteraction treats them separately:

  1. The part flickers when the ray lands near an edge two parts share, or grazes a silhouette. A pixel's worth of hand tremor alternates the answer every frame, and a label that alternates every frame is unreadable. The fix is a dwell guarding every transition away from a held hover — see PointerInteraction.switchSamples and the rule stated on resolveHover. Acquiring a hover is never delayed, because a lag between pointing at something and it lighting up reads as the pointer being broken.
  2. The click flickers when a continuous select value sits on the threshold — which a geometric pinchStrength derived from a 40 mm range of finger separation does constantly. The fix is a Schmitt trigger: press at PointerInteraction.pressThreshold, release at the lower PointerInteraction.releaseThreshold. A value wobbling between them holds its state.

And once pressed, the drag belongs to what it grabbed: hover is frozen, and PointerInteraction.dragOffset reports where the ray now crosses the plane through the original contact point, so letting the pointer wander off the object does not drop the drag.

Typical use

val scene = pickScene { add("model", mesh, parts = builder.groups()) }
val pointer = mousePointer(camera = { view.rayCamera(mesh) }, cursor = { window.cursor },
                           selecting = { window.isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) })
val interaction = PointerInteraction(scene)
forever {
    interaction.update(pointer.sample())
    if (interaction.justPressed) println("grabbed ${interaction.grabbed?.partName}")
}

src/jvmMain/kotlin/afield/graphics/interaction/pointing_pointer.kt — read the source

Top-level functions

Declaration Summary
fun mousePointer(name: String = "mouse", camera: () -> RayCamera?, cursor: () -> Vec2, selecting: () -> Boolean, clampToFrame: Boolean = true): Pointer A mouse, made into a spatial pointer by inverting a camera.
fun controllerPointer(name: String = "controller", state: () -> ControllerState?, localForward: Vector3f = Vector3f(0f, 0f, 1f), select: (ControllerState) -> Double = { s -> maxOf(s.triggerValue.toDouble(), if (s.selectPressed || s.triggerPressed) 1.0 else 0.0) }): Pointer An XR controller's aim, and its trigger.
fun pinchPointer(name: String = "pinch", state: () -> HandTrackingState?, eye: (() -> Vec3)? = null): Pointer A bare hand: aim by moving it, select by pinching. The first consumer of afield.graphics.vulkan.targets.OpenXRHmdTarget.handState.
fun fixedPointer(name: String = "fixed", fixed: () -> PointerState): Pointer A pointer that always reports fixed — for tests, and for driving a capture from a script.
fun afield.graphics.vulkan.targets.SideBySideStereoTarget.pointer(hand: afield.graphics.vulkan.targets.OpenXRHmdTarget.Hand = afield.graphics.vulkan.targets.OpenXRHmdTarget.Hand.RIGHT, name: String = "controller ${hand.name.lowercase()}"): Pointer This stereo target's controller, as a Pointer.
fun afield.graphics.vulkan.targets.OpenXRHmdTarget.handPointer(hand: afield.graphics.vulkan.targets.OpenXRHmdTarget.Hand = afield.graphics.vulkan.targets.OpenXRHmdTarget.Hand.RIGHT, name: String = "pinch ${hand.name.lowercase()}"): Pointer This headset's bare hand, as a Pointer — pinch to select.

PointerState

class PointerState(val ray: Ray?, val select: Double = 0.0)

One reading of a pointing device.

Declaration Summary
val ray: Ray? Where the pointer is aiming, or null when it is not there.
val select: Double How hard the person is selecting, 0..1. A mouse button is 0 or 1; a trigger and a pinch are continuous, which is why PointerInteraction needs two thresholds rather than one.
val active: Boolean True when there is a ray to use.
override fun toString(): String

Pointer

interface Pointer

Something a person points with. Sampled, not subscribed to — see PointerInteraction.

Declaration Summary
val name: String For logs and for telling two hands apart.
fun sample(): PointerState Read the device now.

LambdaPointer

class LambdaPointer(override val name: String, private val read: () -> PointerState) : Pointer

A Pointer from a lambda. What every factory below builds.

Declaration Summary
override fun sample(): PointerState
override fun toString(): String

PointerPhase

enum class PointerPhase

What a PointerInteraction is doing.

Constants: IDLE, HOVER, PRESSED, DRAGGING.

PointerInteraction

class PointerInteraction(var scene: PickScene, val pressThreshold: Double = 0.6, val releaseThreshold: Double = 0.35, val switchSamples: Int = 3, val maxDistance: Double = Double.MAX_VALUE, val cullBackFaces: Boolean = false)

A stream of PointerStates turned into an interaction: what is hovered, what is grabbed, and what a drag has moved by.

Declaration Summary
var scene: PickScene What to pick against. Swap it freely between calls; the machine holds hits, not indices.
val pressThreshold: Double Select value at which a press begins. Above releaseThreshold by construction.
val releaseThreshold: Double Select value at which a press ends. The lower half of the Schmitt trigger: a value wobbling between the two thresholds holds whichever state it is in.
val switchSamples: Int How many consecutive samples a different part must be the nearest hit before the hover moves to it.
val maxDistance: Double Ignore anything further away than this.
val cullBackFaces: Boolean Skip triangles facing away, as a renderer would.
var phase: PointerPhase What the machine is doing.
var hover: PickHit? What the pointer is over. Frozen to grabbed while PointerPhase.PRESSED or PointerPhase.DRAGGING — a drag belongs to what it grabbed.
var grabbed: PickHit? What the press landed on, or null when nothing is pressed.
var ray: Ray? The last sample's ray, for whoever wants to draw it.
var justPressed: Boolean True for exactly the one update in which the press began.
var justReleased: Boolean True for exactly the one update in which the press ended with the pointer still there — i.e. a click. A pointer that vanished mid-press sets justCancelled instead.
var justCancelled: Boolean True for exactly the one update in which a press was abandoned because the pointer went away.
var dragOffset: Vec3? How far the drag has moved the grabbed point, in world units, or null when nothing is grabbed.
val dragDistance: Double Straight-line length of dragOffset, or 0.
fun update(state: PointerState, moveEpsilon: Double = 1e-4): PointerPhase Feed one sample. Returns the new phase.
override fun toString(): String

PaintCanvas.kt

PaintCanvas, a simulated oil-painting canvas that FLines are painted and drawn onto, with its statistics (PaintStats, PaperInfo) and pngRow for showing several canvases side by side.

The canvas runs on the optional native wrapper native/field-paint; see PaintCanvas for building it. Tests: PaintCanvasTest, GesturePaintReplayTest, StereoTest.

src/jvmMain/kotlin/afield/graphics/paint/PaintCanvas.kt — read the source

Used in: Watercolour and ink

Top-level functions

Declaration Summary
fun pngRow(canvases: List<PaintCanvas>, gap: Int = 12, background: Int = 0xffffffff.toInt(), wet: Boolean = false, relief: Double = 0.5): ByteArray canvases rendered side by side, top-aligned, gap pixels apart on background (ARGB), as one PNG: for comparing papers, brushes or pigments in one picture. wet renders each as it is now rather than dried; relief lights a dried render's surface as PaintCanvas.png does.

PaintCanvas

class PaintCanvas internal constructor(internal val handle: MemorySegment, val aspect: Double, seed: Long, var view: Rect?) : AutoCloseable

A simulated oil-painting canvas that FLines are painted and drawn onto.

Declaration Summary
val aspect: Double width / height.
var view: Rect? The rectangle of FLine coordinates that maps onto the canvas, or null for canvas units.
val pixelSize: Pair<Int, Int> Pixel width and height of the picture render returns.
val widthPx: Int Pixel width.
val heightUnits: Double Height in canvas units (the width is always 1000).
val paintEngine: Int by lazy The engine version this canvas paints with (PaintCanvas.invoke's engine).
fun brush(kind: BrushKind = BrushKind.Round, width: Double = 20.0, point: Double = 0.0, config: BrushConfig? = null, seed: Long? = null): PaintBrush A brush held over this canvas, width canvas units wide whatever the view. Load it with PaintBrush.load before stroking. config changes how it is made — how many bristles, how stiff and long, how much it lays and picks up — from the kind's own.
fun knife(width: Double = 60.0): PaintKnife A painting knife whose blade is width canvas units long (a 4 cm blade is about 60 units on a 700 mm canvas). Tool sizes are canvas units whatever the view, as a brush's width is.
fun rag(width: Double = 50.0, seed: Long? = null): PaintRag A clean cotton rag bunched into a pad width canvas units across.
fun pencil(grade: String = "HB"): Pencil A graphite pencil of grade ("2H", "HB", "4B", ...) for draw, sketch and hatch. Hard grades lay a pale grey and stay sharp; soft ones lay more, darker, and blunt as they draw.
fun chalk(): Pencil Natural black chalk: deep, matte, broad and crumbly; it wears fast.
fun stroke(line: FLine, brush: PaintBrush, pressure: ClosedFloatingPointRange<Double> = 0.8..0.8, shake: Double = 1.0, attack: Double = 0.08, release: Double = 0.15, along: Boolean = false, swell: List<Double> = emptyList(), spacing: Double = 2.0, seed: Long? = null): PaintCanvas Drag brush along every subpath of line.
fun touch(brush: PaintBrush, x: Double, y: Double, pressure: Double = 0.6, drag: Pair<Double, Double> = 0.0 to 0.0, twist: Double = 0.0, angle: Double = 0.0, seed: Long? = null): PaintCanvas Touch the brush tip down at (x, y) and lift it, with no path: a dab. Harder pressure makes a bigger, fuller mark; drag moves the hand that far while the hairs are down, twist rolls the handle (radians) and angle turns a flat's wide axis (radians).
fun work(region: FLine, color: Vec4, handling: Handling = Handling.Body, hiding: Double = 0.85, stiff: Double = 0.8, angle: Double? = null, angleJitter: Double? = null, width: Double? = null, length: ClosedFloatingPointRange<Double>? = null, coverage: Double? = null, pressure: ClosedFloatingPointRange<Double>? = null, cross: Double? = null, order: StrokeOrder? = null, clip: Boolean? = null, feather: Double = 0.0): PaintCanvas Work a whole region with many strokes, the way a painter blocks in an area: the crate plans the strokes (lengths, directions, curvature, broken and restated strokes, trips to the palette) from a Handling preset, and paints each with a simulated brush.
fun outline(line: FLine, brush: PaintBrush?, character: OutlineCharacter = OutlineCharacter.Firm, pressure: Double = 0.7, amount: Double? = null, closed: Boolean? = null, size: Double? = null): FLine Draw a hand outline through the rough points of line — each subpath one outline, closed when its last point returns to its first or closed says so — and drag brush along the strokes a hand would use: they lift and restart, overlap or leave gaps, overshoot corners and restate stretches, each with its own pressure. Returns those strokes as an FLine (in the same coordinates as line), so the drawing can be reused; pass brush = null to get the strokes without painting them.
fun glaze(color: Vec4, region: FLine? = null, coats: Double = 1.0, hiding: Double = 0.05, feather: Double = 0.0): PaintCanvas A transparent Kubelka–Munk glaze of color, coats thick, over region (or the whole canvas). It dries at once. Glaze over dry paint: call dry first if the paint under it is still wet.
fun spatter(brush: PaintBrush, x: Double, y: Double, toward: Pair<Double, Double>, spread: Double = 0.35, force: Double = 0.7): Int Flick brush at (x, y) towards toward (a direction whose length is how far the paint carries): the paint the hairs can't hold by capillarity flies off in droplets and lands in a cone spread radians either side. Only the loose paint flies — more of it the harder the force (0..1) and the more fluid the paint (thinned or turps-rich paint spatters, stiff tube paint barely leaves the brush) — and a harder flick breaks it into more, smaller drops. Each drop comes off one hair with that hair's paint, so a double-loaded brush spatters both colours. Returns how many droplets landed.
fun knife(line: FLine, knife: PaintKnife, pressure: ClosedFloatingPointRange<Double> = 0.6..0.6, angle: Double? = null, lay: Boolean = true, lift: Double = 0.3, seed: Long? = null): PaintCanvas Pull knife along every subpath of line, pressed pressure from start to end, the blade across the path or at angle (radians). The flexible blade rests on the high points of the dry surface and stands off them by a gap that closes as it is pressed (a light touch rides about 0.3 mm up). Wet paint above the blade is cut into its bead, or pressed out past its ends into ridges; with lay, below it the bead fills the surface to the blade's level: a flat-topped slab, or paint on the peaks of dry impasto only. When the blade lifts, lift of the bead stays where it was.
fun scrape(line: FLine, knife: PaintKnife, pressure: ClosedFloatingPointRange<Double> = 0.9..0.9, angle: Double? = null, seed: Long? = null): PaintCanvas Scrape wet paint off along line with knife, pressed hard: knife with nothing laid.
fun wipe(line: FLine, rag: PaintRag, pressure: List<Double> = listOf(0.6), seed: Long? = null): Double Wipe rag along every subpath of line at pressure (one value, or values spread along the path). The pad rests on the tops of the weave and reaches into the hollows as it is pressed, and the cloth touches in creases, so a wipe lifts in streaks. Returns the mm³ of paint lifted: negative where a loaded rag left more than it took up, smearing its paint onto paint that is setting.
fun blot(rag: PaintRag, x: Double, y: Double, pressure: Double = 0.6, seed: Long? = null): Double Press rag down at (x, y) and lift it off: a crumpled patch. Returns the mm³ lifted.
fun dampen(rag: PaintRag, amount: Double = 0.5): PaintCanvas Dip rag's face in spirits, amount 0..1: damp, it lifts paint nearly to the ground until the spirits evaporate.
fun refold(rag: PaintRag): PaintCanvas Turn a cleaner face of rag outward (no face is cleaner than what has soaked through the cloth).
fun ragState(rag: PaintRag): RagState How loaded and damp rag is now, on this canvas's clock.
fun carried(brush: PaintBrush): Carried The paint brush carries, in mm³ on this canvas's physical scale, and its mixed colour.
fun carried(knife: PaintKnife): Carried The paint on knife's blade, as carried for a brush.
val mmPerUnit: Double Millimetres per canvas unit.
fun waterBrush(width: Double = 30.0, capacityMm3: Double? = null): WaterBrush A round watercolour brush width canvas units across. Its belly holds about 0.6 d³ mm³ of water for a brush d mm wide (a 6 mm round about 0.13 ml); capacityMm3 sets it instead.
fun gesture(line: FLine, speed: Double = 80.0, pressure: List<Double> = listOf(0.6), gap: Double = 0.3, spacing: Double = 4.0, tilt: Double = 0.0, lean: Boolean = true, roll: Double? = null, facets: Double? = null): TimedGesture A TimedGesture along every subpath of line (one after another, gap seconds apart), moving at speed mm/s, pressing pressure (one value, a start and end, or a profile at evenly spaced stations), sampled every spacing canvas units. The handle is tilted tilt degrees from upright; with lean it leans toward where it is going (the hairs trailing behind, as a brush is pulled), and a flat's wide axis is held across the path unless roll (degrees) fixes it. Where the path doubles back on itself by more than 120°, as in scrubbing, the handle keeps its lean and the brush is pushed back.
fun gesture3d(line: FLine, project: (Vec3) -> Vec2, mmPerWorld: Double = 1.0, speed: Double = 80.0, pressure: List<Double> = listOf(0.6), gap: Double = 0.3, spacing: Double = 2.0, tilt: Double = 0.0, lean: Boolean = true, roll: Double? = null): TimedGesture A TimedGesture along a line in three dimensions, as one eye of a stereo pair sees it: project takes a point of line (world units, z included) to this canvas's FLine coordinates. Lengths, speed and the pressure profile are the world's, measured along the line in 3D at mmPerWorld mm to a world unit: each sample carries its world arc (TimedGesture.Sample.s), so the two eyes' gestures press, pause and run out at the same places along the line however foreshortened each sees it. Each subpath is one movement of the hand (gap seconds apart); straight segments are sampled every spacing world units, curves finely enough to follow. The handle's tilt, lean and roll are as in gesture, its heading taken on this canvas.
fun warp(warp: StereoWarp, ground: Boolean = true): PaintCanvas Make this canvas one eye of a stereo pair whose other eye (the reference) is painted without a warp: from now on texture keyed on the canvas's places (a pencil's grain, tacky paint's grab) is looked up where warp says each place is on the reference. With ground, the ground (its weave, priming and colour, or a watercolour sheet's relief and fibres) is pulled back through it too, as if made on the reference, so give the warp to a fresh canvas, before any paint. warp's canvas units are this canvas's own (1000 across), not its view's.
fun warp(pins: List<StereoWarp.Pin>, cell: Double = 10.0, frame: Boolean = true, ground: Boolean = true): StereoWarp The StereoWarp through pins (this canvas's FLine coordinates, its view if it has one), solved over this canvas (StereoWarp.solve), and given to it (warp).
fun stroke(gesture: TimedGesture, brush: PaintBrush, shake: Double = 1.0, attack: Double = 0.08, release: Double = 0.15, along: Boolean = false, seed: Long? = null): PaintCanvas Drag brush through gesture (e.g. from gesture3d): each movement of the hand one stroke, pressed as the gesture presses, and what is laid out along it following its world arc.
fun knife(gesture: TimedGesture, knife: PaintKnife, angle: Double? = null, lay: Boolean = true, lift: Double = 0.3, seed: Long? = null): PaintCanvas Pull knife through gesture, as along a line, its tears and pressure following its world arc.
fun wipe(gesture: TimedGesture, rag: PaintRag, seed: Long? = null): Double Wipe rag through gesture, its creases, wander and pressure following its world arc. Returns the mm³ lifted.
fun sketch(gesture: TimedGesture, pencil: Pencil, passes: Int = 3, wander: Double = 2.0, tremor: Double = 0.4, seed: Long? = null): PaintCanvas Sketch along gesture (sketch), its searching strokes laid out along its world arc: a stereo pair's eyes break and wander the strokes at the same places along the line. The pressure is the gesture's mean.
fun draw(gesture: TimedGesture, pencil: Pencil, tremor: Double = 0.4, ruler: Boolean = false, seed: Long? = null): PaintCanvas Draw gesture with pencil, its tremor and pressure following its world arc.
fun wash(line: FLine, brush: WaterBrush, pressure: List<Double> = listOf(0.6), speed: Double = 80.0, seed: Long? = null): WashReport Drag a watercolour brush along line at speed mm/s (gesture), the water on the sheet flowing, sinking in and evaporating as it goes. What it laid and took up comes back.
fun wash(gesture: TimedGesture, brush: WaterBrush, seed: Long? = null): WashReport Drag a watercolour brush through a timed gesture.
fun dynamicBrush(kind: BrushKind = BrushKind.Round, width: Double = 30.0, hairs: Int = 32, segments: Int = 5, config: BrushConfig? = null, seed: Long? = null): DynamicBrush A brush of kind whose hairs move (DynamicBrush): hairs guide hairs of segments segments each, made as config says (its stiffness, length, point and so on).
fun stroke(gesture: TimedGesture, brush: DynamicBrush, seed: Long? = null, splay: ((TimedGesture.Sample) -> Double)? = null, splayFan: ((TimedGesture.Sample) -> Double)? = null, splayScatter: ((TimedGesture.Sample) -> Double)? = null, cohesion: ((TimedGesture.Sample) -> Double)? = null, clumping: ((TimedGesture.Sample) -> Double)? = null): DynamicReport Drag a DynamicBrush through a timed gesture, in oil or watercolour as it was last loaded.
fun stroke(line: FLine, brush: DynamicBrush, pressure: List<Double> = listOf(0.6), speed: Double = 80.0, tilt: Double = 30.0, seed: Long? = null): DynamicReport Drag a DynamicBrush along line at speed mm/s, tilted tilt degrees and leaning the way it goes (gesture makes the timing and the handle's pose).
fun contactMap(): FloatArray? Seconds of hair contact per pixel in the last DynamicBrush stroke (row major), or null before one.
fun contactMapPng(): ByteArray contactMap as a PNG, black where no hair touched and white at the most-touched pixel.
fun hairs(brush: DynamicBrush): List<List<Triple<Double, Double, Double>>> brush's hairs as they are now: each a polyline of its nodes, x and y in this canvas's coordinates and z the height above the canvas in mm. Draw them with FLines to see the brush.
fun drop(x: Double, y: Double, paint: WaterPaint, volumeMm3: Double = 20.0): PaintCanvas Let a drop of volumeMm3 µl of paint fall at (x, y): it spreads to a puddle, then the sheet takes it.
fun wet(region: FLine, microns: Double = 100.0, paint: WaterPaint = WaterPaint.Clear, feather: Double = 0.0): PaintCanvas Lay microns µm of water over region at once, as a wide mop would: clean water to work wet into wet, or a flat wash of paint.
fun tilt(degrees: Double, direction: Double = 90.0): PaintCanvas Tilt the sheet degrees from flat, falling toward direction degrees (90: down the canvas). Upright is 90°.
fun evaporation(micronsPerSecond: Double): PaintCanvas Set how fast open water evaporates, µm/s: still air at 20 °C and 50 % humidity about 0.15, a warm draught several times that.
fun marangoni(newtonsPerMetre: Double): PaintCanvas How far paint lowers the surface tension of the water it is in, N/m (0.008 by default; clean water is 0.072). Cleaner water pulls the surface toward it — Marangoni flow — so paint touched into a wet wash spreads into it: wet-in-wet's blooms. 0 turns it off; more spreads faster.
fun dispersion(mm2PerSecond: Double): PaintCanvas An extra spreading of pigment through standing water, mm²/s, 0 by default. Not simulated: a dial for mixing the water model does not resolve, added to the Marangoni spreading.
fun waitSeconds(seconds: Double): PaintCanvas Let seconds pass: water flows, sinks in and evaporates; oil paint ages with it.
val water: WaterLedger? The water's books on this canvas, or null before any water touched it.
val paper: PaperInfo? The paper under the water: its make-up, or null before any water touched the canvas.
fun waterMap(map: WaterMap): FloatArray? One value per pixel of the water's state (row major, pixelSize); null before any water touched the canvas.
fun waterMapPng(map: WaterMap, max: Double? = null): ByteArray A water map as a PNG, black at 0 and white at max (the map's own maximum when null), for looking at what the water is doing.
fun draw(line: FLine, pencil: Pencil, pressure: List<Double> = listOf(0.6), tremor: Double = 0.4, ruler: Boolean = false, seed: Long? = null): PaintCanvas Draw line with pencil: each subpath one line, smoothed through its points and wavering a little (tremor canvas units), its pressure following pressure (one value, a start and end, or a profile through evenly spaced stations). A light line catches only the tops of the ground's tooth and breaks up into its grain; a heavy one fills in. The pencil wears as it draws. Graphite skips wet paint.
fun draw(line: FLine, pencil: Pencil, pressure: Double, tremor: Double = 0.4, ruler: Boolean = false, seed: Long? = null): PaintCanvas draw with one pressure for the whole line.
fun sketch(line: FLine, pencil: Pencil, pressure: Double = 0.5, passes: Int = 3, wander: Double = 2.0, tremor: Double = 0.4, seed: Long? = null): PaintCanvas Sketch along line: passes light strokes per subpath, each wandering about wander canvas units off the path and broken into pieces, the way a searching line is drawn.
fun hatch(region: FLine, pencil: Pencil, angle: Double = 0.8, spacing: Double = 6.0, length: Double = 60.0, pressure: Double = 0.6): PaintCanvas Hatch region: short parallel strokes at angle (radians), spacing canvas units apart, each at most length long, bowed a little, heavy where they start and lifting off at the end.
fun erase(region: FLine, strength: Double = 0.8, feather: Double = 0.0): PaintCanvas A kneaded eraser over region: lifts strength (0..1) of the loose drawing, better from the tops of the tooth than the hollows, and never quite all of it. Fixed or painted-over drawing stays.
fun fixDrawing(): PaintCanvas Spray fixative over the drawing: an eraser no longer lifts it.
fun stipple(region: FLine, color: Vec4, size: Double = 3.0, coverage: Double = 1.0, hiding: Double = 0.4, stiff: Double = 0.3, pressure: ClosedFloatingPointRange<Double> = 0.4..0.75, cluster: Double = 0.15, drag: Double = 0.0, dipEvery: Int = 24, feather: Double = 0.0): PaintCanvas Stipple region with many separate touches of an upright stippling brush size canvas units wide: coverage touches per point on average, in color. Touches into wet paint pick some of it up and mix with it, like every other brush mark.
fun prime(color: Vec4, hiding: Double = 0.85, microns: Double = 60.0, stiff: Double = 0.4, texture: Double = 0.3, absorbent: Double = 0.0): PaintCanvas Lay a ground over the whole canvas: microns µm of color at hiding, with body stiff (fluid chalk-glue ≈ 0.2, oil lead white ≈ 0.6), roughened by texture before it levels. Dries the canvas first. absorbent (0..1, engine 4) makes it a chalk and glue ground that draws oil out of the paint laid on it — up to 0.6 coats' worth — so that paint dries leaner, stiffer and matte, until paint seals it.
fun crack(kind: CrackKind = CrackKind.Aged): PaintCanvas Age the painting: craquelure grown through paint and ground in a stress field, cupped, with dirt in the cracks. CrackKind.Aged is a hierarchy of cracks developing patchily; CrackKind.Even one uniform network. Fitted to the ground's thickness, so it reads best on an oil ground. Dries the canvas first.
fun wait(minutes: Double): PaintCanvas Let minutes of simulated time pass: wet paint stiffens, levels and sets where it lies.
fun dry(): PaintCanvas Wait until every film is touch-dry. Paint laid afterwards no longer mixes with it.
val stats: PaintStats Where the painting stands: its clock, the hand time spent, and how much paint is still wet.
fun render(relief: Double = 0.0, gloss: Double = 0.0, wet: Boolean = false, light: PaintLight? = null): IntArray The picture as sRGB ARGB pixels, row major, pixelSize wide and high.
fun png(relief: Double = 0.6, gloss: Double = 0.0, wet: Boolean = false, light: PaintLight? = null): ByteArray render encoded as PNG — what display("main", ...) shows as an image.
fun save(path: String) Save a dried copy with the crate's own dithered PNG writer (no relief).
fun copy(): PaintCanvas An independent copy of this canvas, wet paint and all: try something, and keep the original.
fun saveState(path: String) Write the whole state, wet paint included, so PaintCanvas.load can carry on from here.
override fun close()

PaintStats

data class PaintStats(val clockMinutes: Double, val handMinutes: Double, val open: Double, val setting: Double, val tacky: Double, val dry: Double, val wetPaintMm3: Double = 0.0, val solventMm3: Double = 0.0, val engine: Int = 0)

Clock, hand time and drying state of a PaintCanvas. Shares are of the canvas's area.

Declaration Summary
val clockMinutes: Double Simulated minutes passed (PaintCanvas.wait, PaintCanvas.dry).
val handMinutes: Double An estimate of the hand time the marks so far would take a painter, in minutes.
val open: Double Workable wet paint: blends, lifts, levels.
val setting: Double Near its gel point: stiff and sticky, barely blends.
val tacky: Double A set film: doesn't move, grabs the brush.
val dry: Double Touch-dry paint, or bare ground.
val wetPaintMm3: Double Wet oil paint on the canvas, mm³.
val solventMm3: Double Solvent (thinner) still in the open films, mm³.
val engine: Int The engine the canvas paints with.

PaperInfo

data class PaperInfo(val color: Vec4, val capacityMicrons: Double, val absorbMicronsPerSecond: Double, val wickMm2PerSecond: Double, val anisotropy: Double, val fibreMm: Double, val formation: Double, val roughnessMicrons: Double, val textureMm: Double, val pinMicrons: Double)

The make-up of the sheet under a canvas's water (PaintCanvas.paper).

Declaration Summary
val color: Vec4 The sheet's own colour, sRGB: what a wash is laid over and what shows through it.
val capacityMicrons: Double Water it holds saturated, µm.
val absorbMicronsPerSecond: Double How fast dry paper takes up standing water, µm/s.
val wickMm2PerSecond: Double In-plane capillary diffusivity at saturation, mm²/s.
val anisotropy: Double Along the grain over across it.
val fibreMm: Double Fibre length, mm. It is xuan's long fibres that feather a drop out along the grain.
val formation: Double
val roughnessMicrons: Double The tooth: how deep the paper's texture runs, µm. A full brush floods across all of it; a nearly dry one touches only the top, which is drybrush.
val textureMm: Double How wide the texture's features are, mm. It is a real length on a real sheet, so the same strokes on a smaller sheet look coarser.
val pinMicrons: Double Depth of water needed to advance onto dry paper, µm.

PaintNoise.kt

Smooth 1D noise that varies a stroke along its length, the same for the same seed, so the two eyes of a stereo pair can share it: PaintNoise.

src/jvmMain/kotlin/afield/graphics/paint/PaintNoise.kt — read the source

PaintNoise

object PaintNoise

Smooth noise for varying a stroke along itself (PaintCanvas.stroke's varied behaviours): a value from -1 to 1 that wanders as a stroke goes. Keyed on how far along a movement in the world a sample is (TimedGesture.Sample.s, mm), it lies at the same places along that movement on both canvases of a stereo pair, however long each sees it.

Declaration Summary
fun along(mm: Double, scaleMm: Double = 60.0, seed: Long = 0, octaves: Int = 3): Double Smooth noise at mm along a stroke, -1..1, wandering over about scaleMm (a few octaves, each half the last's scale and strength), the same for the same seed.
fun along(sample: TimedGesture.Sample, scaleMm: Double = 60.0, seed: Long = 0, octaves: Int = 3): Double along at a gesture's sample: by its world length, or its time where it has none.

PaintToolbox.kt

Named painting tools that keep their state between strokes, and can be saved to a file and loaded back: PaintToolbox.

src/jvmMain/kotlin/afield/graphics/paint/PaintToolbox.kt — read the source

PaintToolbox

class PaintToolbox : AutoCloseable

Tools kept by name between strokes: a brush stays dirty with what it last picked up, a knife keeps its bead, a rag its load, a pencil its wear, a watercolour brush its water and pigment, until they are reloaded, wiped or replaced.

Declaration Summary
val names: List<String> The tools' names, in the order they were added.
operator fun get(name: String): PaintTool? The tool called name, or null.
operator fun set(name: String, tool: PaintTool) Keep tool as name, putting away (closing) whatever had that name.
operator fun contains(name: String): Boolean
fun remove(name: String) Put away the tool called name.
fun brush(name: String): PaintBrush The brush called name.
fun knife(name: String): PaintKnife The knife called name.
fun rag(name: String): PaintRag The rag called name.
fun pencil(name: String): Pencil The pencil (or chalk) called name.
fun waterBrush(name: String): WaterBrush The watercolour brush called name.
fun dynamicBrush(name: String): DynamicBrush The brush of moving hairs called name.
val entries: List<Pair<String, PaintTool>> Every tool as name to tool.
fun save(file: File) Write every tool, exactly, to file.
fun write(out: DataOutputStream) Write every tool, exactly, to out.
override fun close() Put every tool away.
fun describe(canvas: PaintCanvas): String One line per tool, for an agent or a person to read: what it is, and what it carries on canvas.

PaintTools.kt

The tools and materials a PaintCanvas is worked with: grounds, brushes, knives, rags, pencils, and tubes of paint.

A tool is made by its canvas (PaintCanvas.brush, PaintCanvas.knife, PaintCanvas.rag, PaintCanvas.pencil, PaintCanvas.waterBrush, PaintCanvas.dynamicBrush) and the native ones are AutoCloseable. BrushKind and BrushConfig shape a bristle brush, Handling says how it is moved, and Tubes names ready-mixed colours to load it with. Tests: PaintToolsTest, DynamicBrushTest.

src/jvmMain/kotlin/afield/graphics/paint/PaintTools.kt — read the source

Used in: Paint and pencil from FLines, Drawn Strokes Become Paint, Brushes with moving hairs

PaintGround

enum class PaintGround(internal val code: Int)

What a PaintCanvas starts as.

Constants: Bare, Oil, OilRedGround, Linen, RawCotton, RawLinen.

BrushKind

enum class BrushKind(internal val code: Int)

The brushes the crate models.

Constants: Round, HogFlat, Filbert, Fan, Rigger, Badger, Stippler.

BrushConfig

data class BrushConfig(val bristles: Int? = null, val length: Double? = null, val stiffness: Double? = null, val hair: Double? = null, val run: Double? = null, val lay: Double? = null, val pickup: Double? = null, val push: Double? = null, val splay: Double? = null, val ragged: Double? = null, val point: Double? = null, val tooth: Double? = null)

How a brush is made, beyond its kind and width: any field left null keeps the kind's own. Lengths are canvas units; the rest are the crate's own scales.

Declaration Summary
val bristles: Int? Number of simulated bristles (1..4000).
val length: Double? How far the tips trail behind the contact at full pressure.
val stiffness: Double? 0 soft hair (sable, badger) .. 1 stiff hog bristle.
val hair: Double? Bristle thickness multiplier: how much neighbouring hairs overlap.
val run: Double? The stroke length over which a full load runs down (e-folding), canvas units.
val lay: Double? Paint thickness laid at the start of a fully loaded stroke (coats).
val pickup: Double? Share of the wet paint under it a bristle takes up per pass.
val push: Double? Share of the wet paint a moving bristle ploughs aside per pass.
val splay: Double? How much the footprint grows under pressure.
val ragged: Double? Unevenness of the bristle lengths: ragged edges and broken marks.
val point: Double? 0 a blunt tuft .. 1 a fine point (the point argument of PaintCanvas.brush, when given here instead).
val tooth: Double? How far a bristle's dryness keeps it on the canvas's tooth: 0 (as made), pressure pushes it into the weave's hollows however dry it is; 1, a bristle running dry rides only the peaks, however hard it is pressed, and breaks into dry-brush skips on a ground with grain (on a smooth one, none: it thins evenly). Past 1 drier still; below 0 the reverse.

LoadPart

data class LoadPart(val side: Double = 0.0, val share: Double = 1.0, val streak: Double = 0.0, val seed: Long? = null)

Which part of a brush a dip reaches (PaintBrush.load): a painter double-loads a brush by dipping one edge in a second pile.

Declaration Summary
val side: Double The edge of the brush's wide axis that goes in: -1 one edge, 1 the other, 0 the whole width alike.
val share: Double The share of the width, from that edge, that reaches the paint.
val streak: Double 0 every bristle in reach takes it up alike; 1 in streaks, as from an unevenly knifed pile.
val seed: Long? Where the streaks fall. By default they follow from the brush as it is (how full it is), so a restored brush dips alike; give a seed to fix them, as two eyes of a stereo pair must.

PaintLight

data class PaintLight(val azimuth: Double = 135.0, val elevation: Double = 25.0, val strength: Double = 1.0)

A light to look at the canvas under (PaintCanvas.render light =): from azimuth degrees (0 from the right, 90 from the top, 135 the upper left) at elevation degrees above the canvas. Relief casts shadows along it, and wet paint has an oily sheen. strength scales the shading.

Handling

enum class Handling(internal val code: Int)

How PaintCanvas.work covers a region: the crate's own presets for its oil painters.

Constants: Broad, Body, Detail, Hatch, Glaze, Scumble, Blend.

StrokeOrder

sealed interface StrokeOrder

The order PaintCanvas.work lays its strokes in.

Declaration Summary
data object Passages : StrokeOrder Patch by patch, the strokes in a patch laid side by side.
data class Sweep(val angle: Double) : StrokeOrder One sweep across the region in direction angle (radians; π/2 is top to bottom).
data object Scatter : StrokeOrder Anywhere, in random order.

OutlineCharacter

enum class OutlineCharacter(internal val code: Int)

How a hand draws an outline (PaintCanvas.outline).

Constants: Firm, Searching, Broken, Soft.

CrackKind

enum class CrackKind(internal val code: Int)

The two kinds of craquelure (PaintCanvas.crack).

Constants: Aged, Even.

DrawingMedium

enum class DrawingMedium(internal val code: Int)

What a Pencil is made of.

Constants: Graphite, Chalk.

Pencil

class Pencil internal constructor(val medium: DrawingMedium, val softness: Double) : PaintTool

A pencil or stick of chalk for PaintCanvas.draw, PaintCanvas.sketch and PaintCanvas.hatch. Get one from PaintCanvas.pencil or PaintCanvas.chalk. It remembers how far it has drawn, and its line widens as it blunts; sharpen starts it fresh.

Declaration Summary
var worn: Double Millimetres drawn since the pencil was last sharpened.
fun sharpen(): Pencil Sharpen the point.

PaintTool

sealed interface PaintTool

Something a painter holds and puts down again: a PaintBrush, DynamicBrush, WaterBrush, PaintKnife, PaintRag or Pencil.

PaintBrush

class PaintBrush internal constructor(internal val handle: MemorySegment, val kind: BrushKind) : PaintTool, AutoCloseable

A brush held over a PaintCanvas, carrying paint between strokes.

Declaration Summary
val width: Double Width at full pressure, canvas units.
val config: BrushConfig How the brush is made: every construction value as it is, none left to the preset.
fun load(color: Vec4, hiding: Double = 0.92, stiff: Double = 0.8, drying: Double = 1.0, amount: Double = 1.0, wipe: Double = 0.85, thinner: Double = 0.0, turps: Double = 0.0, oil: Double = 1.0, part: LoadPart? = null): PaintBrush Dip the brush in paint of masstone color (sRGB, as rgb(...) gives).
fun load(tubes: Map<String, Double>, medium: Double = 0.0, amount: Double = 1.0, wipe: Double = 0.85, thinner: Double = 0.0, turps: Double = 0.0, oil: Double = 1.0, part: LoadPart? = null): PaintBrush Dip the brush in a pile mixed from tube paints, by name and parts by volume — load(mapOf("lead white" to 3.0, "yellow ochre" to 1.0)). The tubes mix the way paint does (Mixbox, weighted by each pigment's tinting strength), and the pile keeps their hiding, stiffness and drying rate. medium (0..1) thins it with oil. Tubes.names lists the tubes. thinner, turps, oil and part are as in the other load.
fun wipe(fraction: Double = 0.85): PaintBrush Wipe fraction of the paint out of the brush on a rag, without loading any.
val fullness: Double Paint left in the brush, relative to a full load.
fun state(): ByteArray A snapshot of the brush, bristle by bristle: restore makes a brush that carries on exactly as this one.
override fun close()

PaintKnife

class PaintKnife internal constructor(internal val handle: MemorySegment) : PaintTool, AutoCloseable

A painting knife: a flexible steel blade width canvas units long, and the bead of paint it carries. It lays paint in flat slabs with ridges where it is pressed out at the blade's ends and a bead where it lifts; pressed hard without paint it scrapes the wet paint off down to the dry surface. On engine 5 a slab tears where it parts from the blade. Get one from PaintCanvas.knife.

Declaration Summary
fun load(color: Vec4, hiding: Double = 0.92, stiff: Double = 0.9, drying: Double = 1.0, amount: Double = 0.6, wipe: Boolean = true, thinner: Double = 0.0, turps: Double = 0.0, oil: Double = 1.0): PaintKnife Pick up amount (0..1 of a full bead: about a millilitre on a 4 cm blade) of paint of masstone color. wipe cleans the blade first; otherwise it mixes into what is there.
fun load(tubes: Map<String, Double>, medium: Double = 0.0, amount: Double = 0.6, wipe: Boolean = true): PaintKnife load from tube paints, by name and parts by volume.
fun wipe(): PaintKnife Wipe the blade clean.
val fullness: Double Paint on the blade, relative to a full bead.
fun state(): ByteArray A snapshot of the blade and its bead, for restore.
override fun close()

PaintRag

class PaintRag internal constructor(internal val handle: MemorySegment) : PaintTool, AutoCloseable

A soft cotton rag bunched into a pad width canvas units across, wiped or pressed over open paint to lift it (PaintCanvas.wipe, PaintCanvas.blot). What it lifts soaks into the cloth: the face in use loads up and lifts less until it is refolded (PaintCanvas.refold); dampened with spirits (PaintCanvas.dampen) it lifts nearly to the ground. Only wet paint comes away: past its gel point paint is part of the dry picture.

Declaration Summary
fun state(): ByteArray A snapshot of the rag, for restore.
override fun close()

WaterBrush

class WaterBrush internal constructor(internal val handle: MemorySegment) : PaintTool, AutoCloseable

A round watercolour brush: a belly that holds water, and the pigment dissolved in it. Where it touches the paper water flows between them by capillary suction, from the wetter to the drier: a full brush lays water and pigment, a damp one on a wet wash drinks it back up (with the pigment floating in it). A fast stroke lays less than a slow one. A light, nearly dry brush reaches only the peaks of the paper's tooth: drybrush comes from the paper, not a texture. Get one from PaintCanvas.waterBrush.

Declaration Summary
fun load(paint: WaterPaint, fill: Double = 1.0, wipe: Double = 1.0): WaterBrush Dip it: first squeeze out wipe (0..1) of what it holds, then take up water to fill of its capacity (more than 1 overloads it) with paint dissolved in it.
fun blot(share: Double = 0.6): WaterBrush Touch it to a towel: it loses share of its water and the pigment in it.
val width: Double Width at full pressure, canvas units.
val capacityMm3: Double Water the belly holds full, mm³.
val waterMm3: Double Water in it now, mm³.
val pigmentUg: Double Pigment in it now, µg.
val fill: Double Its water over its capacity.
fun state(): ByteArray A snapshot of the brush and the water and pigment in it, for restore.
override fun close()

DynamicBrush

class DynamicBrush internal constructor(internal val handle: MemorySegment, val kind: BrushKind) : PaintTool, AutoCloseable

A brush whose hairs move: a few dozen guide hairs simulated as chains of segments under a timed gesture of the handle (TimedGesture: position, pressure, tilt, lean and roll). The hairs bend against the paper and its paint, stick and slip by friction, clump when wet, and keep a bend they were forced past (retained deformation, so a brush once splayed stays splayed for a while). Load it with oil (load: upstream's bristles, following the hairs, carry and lay the paint) or with watercolour (loadWater). Get one from PaintCanvas.dynamicBrush.

Declaration Summary
val widthMm: Double Width across the ferrule, mm.
val lengthMm: Double Hair length out of the ferrule, mm.
val hairCount: Int Guide hairs simulated.
val friction: Pair<Double, Double> Static and kinetic friction against the surface.
val fill: Double How full of paint or water it is, 0..1.
val watercolour: Boolean True when it paints watercolour (it was last loaded with loadWater).
val retainedBend: Double The bend the hairs keep at rest, radians, averaged over every joint: 0 a straight brush.
fun set(friction: Pair<Double, Double>? = null, yieldDegrees: Double? = null, plasticity: Double? = null, recovery: Double? = null, cohesion: Double? = null, clumping: Double? = null, splay: Double? = null, splayFan: Double? = null, splayScatter: Double? = null): DynamicBrush How the hairs behave; null keeps a value. friction is static to kinetic against the surface; a joint bent more than yieldDegrees from its rest angle keeps some of the bend, at plasticity per second; rest bends straighten again at recovery per second; cohesion is how strongly the liquid in a wet brush draws its hairs together: 1 draws a full brush's tips to a point, more pulls harder and closer, 0 none, and below 0 pushes nearby hairs apart.
fun load(color: Vec4, hiding: Double = 0.92, stiff: Double = 0.8, drying: Double = 1.0, amount: Double = 1.0, wipe: Double = 0.85, thinner: Double = 0.0, turps: Double = 0.0, oil: Double = 1.0, part: LoadPart? = null): DynamicBrush Load it with oil paint, as PaintBrush.load.
fun load(tubes: Map<String, Double>, medium: Double = 0.0, amount: Double = 1.0, wipe: Double = 0.85): DynamicBrush Load it with tube paints by parts, as PaintBrush.load.
fun loadWater(canvas: PaintCanvas, paint: WaterPaint, fill: Double = 1.0, wipe: Double = 1.0): DynamicBrush Load it with watercolour, as WaterBrush.load: from now on it paints watercolour, on canvas's scale.
fun state(): ByteArray A snapshot of the whole brush: hairs, their retained bends, and the paint or water in it.
override fun close()

DynamicReport

data class DynamicReport(val substeps: Long, val seconds: Double, val contacts: Long, val maxContact: Double, val waterLaid: Double, val waterTaken: Double, val pigmentLaid: Double, val pigmentTaken: Double)

What a segmented-brush stroke did (PaintCanvas.stroke with a DynamicBrush).

Declaration Summary
val substeps: Long How many steps the engine moved the hairs in — what contacts is summed over.
val seconds: Double
val contacts: Long Hair contacts summed over the substeps.
val maxContact: Double The most of one hair's length that lay on the paper, 0..1.
val waterLaid: Double, val waterTaken: Double, val pigmentLaid: Double, val pigmentTaken: Double Watercolour: mm³ of water laid and taken up, µg of pigment laid and taken up.

RagState

data class RagState(val load: Double, val soaked: Double, val damp: Double)

How loaded a PaintRag is (PaintCanvas.ragState).

Declaration Summary
val load: Double The face in use: 0 clean .. 1 full.
val soaked: Double The whole cloth: 0 .. 1, every face full.
val damp: Double How damp the face is with spirits now, 0 dry .. 1 dipped well.

Carried

data class Carried(val color: Vec4?, val paintMm3: Double, val solventMm3: Double)

Paint carried by a brush or knife (PaintCanvas.carried).

Declaration Summary
val color: Vec4? The paint's colour, mixed across the bristles as paint mixes (sRGB); null when clean.
val paintMm3: Double mm³ of paint.
val solventMm3: Double mm³ of solvent beside it (a thinned load).

Tube

data class Tube(val name: String, val pigment: String, val color: Vec4, val hiding: Double, val stiff: Double, val strength: Double, val drying: Double)

One tube paint in the crate's catalogue. Colours are sRGB.

Declaration Summary
val name: String What the crate calls it, and the name Tubes looks a tube up by.
val pigment: String What it is made of.
val color: Vec4 Its masstone: the colour laid thick.
val hiding: Double How much one coat hides (0 transparent .. 1 opaque).
val stiff: Double Stiffness straight from the tube (0 fluid .. 1 stiff).
val strength: Double Tinting strength relative to an average pigment.
val drying: Double Drying rate relative to average paint.

Tubes

object Tubes

The crate's tube paints, and paint-like colour mixing.

Declaration Summary
val all: List<Tube> Every tube in the catalogue.
val names: List<String> Their names, for PaintBrush.load.
operator fun get(name: String): Tube The tube called name.
fun mix(tubes: Map<String, Double>): Vec4 The masstone of a pile mixed from tubes (name to parts by volume), as paint mixes.
fun mixSpectral(vararg colors: Pair<Vec4, Double>): Vec4 Mix colours as pigments rather than light, by weight: per-wavelength Kubelka–Munk (a port of spectral.js), so blue and yellow make green rather than grey.

StereoWarp.kt

A smooth map between the two canvases of a stereo pair, so canvas texture such as weave and grain sits on the painted surface in both eyes: StereoWarp.

src/jvmMain/kotlin/afield/graphics/paint/StereoWarp.kt — read the source

StereoWarp

class StereoWarp internal constructor(val columns: Int, val rows: Int, val cell: Double, internal val at: FloatArray)

Where each place on one eye's canvas of a stereo pair is on the other's (the reference): a rubber sheet stretched between pins, as dense as a grid every cell canvas units.

Declaration Summary
val columns: Int Grid nodes across the canvas.
val rows: Int Grid nodes down the canvas.
val cell: Double Canvas units between nodes: the sheet is as fine as this, and map interpolates between.
data class Pin(val here: Vec2, val there: Vec2) A place on this canvas (here) and the same place on the reference (there), canvas units.
fun map(p: Vec2): Vec2 Where canvas position p is on the reference canvas.
val largest: Double The largest displacement anywhere, canvas units.

TimedGesture.kt

A hand movement sampled in time, with pressure and pen orientation, which drives a brush: TimedGesture and its TimedGesture.Sample.

src/jvmMain/kotlin/afield/graphics/paint/TimedGesture.kt — read the source

TimedGesture

data class TimedGesture(val samples: List<Sample>)

A movement of the hand in time: where the tool is, when, how hard it presses, and how the handle is held. An afield.geometry.path.FLine gives only a path; flicking, dragging slowly, pausing, pressing and lifting need the time and the orientation too. PaintCanvas.gesture makes one from an FLine at a speed; a pen or an agent can give the samples themselves.

Declaration Summary
data class Sample(val x: Double, val y: Double, val t: Double, val pressure: Double, val tilt: Double = 0.0, val azimuth: Double = 0.0, val roll: Double = 0.0, val s: Double = Double.NaN, val piece: Int = 0) One moment of a gesture.
val duration: Double How long it lasts, seconds.
fun faster(factor: Double): TimedGesture The same gesture factor times as fast.
fun pause(seconds: Double): TimedGesture The same gesture held still for seconds at its end, the tool still down.
fun then(next: TimedGesture, gap: Double = 0.0): TimedGesture This gesture followed by next, gap seconds later: as further movements of the hand (an ordinary brush lifts between them; a brush of moving hairs is carried on through).
fun reversed(): TimedGesture The same path backwards, at the same pace.
val pieces: List<List<Sample>> The movements of the hand, each its run of samples (Sample.piece).

WaterMedia.kt

Watercolour and ink on paper: the Paper kinds, WaterPigments and their Watercolors palette, the WaterPaint mixed from them, and the reports and ledgers a wash returns.

A sheet is made with PaintCanvas.paper and worked with PaintCanvas.waterBrush. WaterMap names the fields of the sheet's state that can be read back. Test: WaterMediaTest.

src/jvmMain/kotlin/afield/graphics/paint/WaterMedia.kt — read the source

Used in: Watercolour and ink, Brushes with moving hairs

Paper

enum class Paper(internal val id: String)

A sheet of paper for watercolour and ink (PaintCanvas.paper). Each is a physical make-up — how much water it holds, how fast it drinks and wicks, its grain, tooth and sizing — not a texture: the same drop behaves differently on each because the water does. The numbers are estimates in the ranges reported for each kind of paper.

Constants: ColdPress, HotPress, Rough, Xuan, Blotting.

WaterPigment

data class WaterPigment(val name: String, val k: List<Double>, val s: Double, val density: Double, val diameterUm: Double, val stain: Double, val lift: Double)

A pigment as water carries it: its optics (Kubelka–Munk absorption k per channel and scattering s, per µg/mm² of pigment) and what decides how it moves — particle density (g/cm³) and diameterUm (dense, coarse pigment settles out onto the paper within a minute; fine pigment stays suspended), stain (per second it adsorbs onto fibres from water in the sheet, after which it can't be lifted) and lift (per second a deposit goes back into still water standing on it; a moving brush lifts much faster).

Declaration Summary
val name: String What to call it. Watercolors uses the common name, and WaterPigment.of takes any.
val k: List<Double> Kubelka–Munk absorption per µg/mm²: three entries, one per channel, in r, g, b order.
val s: Double Kubelka–Munk scattering per µg/mm². Low is a dye, high a body colour.
val density: Double Particle density, g/cm³. With diameterUm, this is what decides how fast it settles.
val diameterUm: Double Particle diameter, µm. Dense, coarse pigment settles onto the paper within a minute; fine pigment stays suspended and travels with the water.
val stain: Double Per second, how much adsorbs onto fibres from water in the sheet; after that it will not lift.
val lift: Double Per second, how much of a deposit goes back into still water standing on it. A moving brush lifts much faster than this.
operator fun times(concentration: Double): WaterPaint In paint concentration µg per mm³ of water (a strong wash is a few).

Watercolors

object Watercolors

Watercolour pigments, by their common names; estimates of each one's physics.

Declaration Summary
val names: List<String> by lazy The names get knows.
operator fun get(name: String): WaterPigment The pigment called name.

WaterPaint

data class WaterPaint(val pigments: Map<WaterPigment, Double>)

Pigments dissolved in water, each at a concentration in µg per mm³ of water. Add two with +.

Declaration Summary
operator fun plus(o: WaterPaint) Both paints at once: concentrations add pigment by pigment, so two washes make a stronger one rather than an average.

WashReport

data class WashReport(val waterLaid: Double, val waterTaken: Double, val pigmentLaid: Double, val pigmentTaken: Double, val seconds: Double)

What a watercolour stroke did, mm³ of water and µg of pigment.

PigmentLedger

data class PigmentLedger(val name: String, val suspended: Double, val carried: Double, val deposited: Double, val glued: Double, val bound: Double, val added: Double, val taken: Double, val runoff: Double, val sealed: Double)

One pigment's books on a sheet, µg.

Declaration Summary
val name: String The pigment these books are for, by WaterPigment.name.
val suspended: Double In the surface water, moving with it.
val carried: Double In the water in the sheet's pores, moving with it.
val deposited: Double Settled out of water still on or in the sheet: water flowing through carries it on.
val glued: Double Settled and dried in place, its gum set: rewetting lifts it slowly, a brush faster.
val bound: Double Stained into the fibres: it no longer lifts.
val added: Double All of it that has been laid on the sheet, however it has since moved.
val taken: Double All of it a brush has lifted back off.
val runoff: Double All of it carried off the edge of the sheet by water that ran away.
val sealed: Double Sealed into the picture under paint laid over it.
val imbalance: Double On and in the sheet, less what the ledger says should be: rounding only.

WaterLedger

data class WaterLedger(val clockSeconds: Double, val surfaceMm3: Double, val absorbedMm3: Double, val addedMm3: Double, val takenMm3: Double, val evaporatedMm3: Double, val runoffMm3: Double, val absorbedInAllMm3: Double, val pigments: List<PigmentLedger>)

The water on a sheet and every volume that came or went, mm³ (µl), at clockSeconds.

Declaration Summary
val clockSeconds: Double The canvas's own clock when these numbers were read. It advances as strokes are painted and as PaintCanvas.waitSeconds and PaintCanvas.wait let time pass, not with wall time.
val surfaceMm3: Double Standing on the sheet right now.
val absorbedMm3: Double In the sheet's pores right now.
val addedMm3: Double All the water a brush has laid down.
val takenMm3: Double All the water a brush has lifted back off.
val evaporatedMm3: Double All the water that has evaporated.
val runoffMm3: Double All the water that has run off the edge of the sheet.
val absorbedInAllMm3: Double All the surface water that has gone into the sheet so far.
val pigments: List<PigmentLedger> The same books kept per pigment, one entry for each the sheet has seen.
val imbalanceMm3: Double Water on and in the sheet, less what the ledger says should be: rounding only.
val dry: Boolean True once no water stands on or in the sheet.

WaterMap

enum class WaterMap(internal val code: Int, val unit: String)

A map of the water's state (PaintCanvas.waterMap): one value per pixel.

Constants: SurfaceWater, AbsorbedWater, Saturation, Pigment, Suspended, Deposited, Bound, Relief, Sealed, Carried, Glued.

gestureReplay.kt

Gesture pad to brush: a take recorded on a gesture(...) pad, or its x and y keyframe lanes after editing, painted onto a canvas as a TimedGesture. The pad's square is laid over a rectangle of the canvas and the take's own timing is kept, so how fast the hand moved is how fast the brush moves.

A pad carries pressure, tilt and twist only when it was made with pen = true, and only where the pointer reported them: a mouse reports a flat 0.5 pressure while down and nothing else, and a dimension that never left its resting value reads as unrecorded. Each one missing is filled the way PaintCanvas.gesture(line, …) fills it from a path, which is to say a steady press, a steady lean toward where the stroke is going, and a flat held across its path.

src/jvmMain/kotlin/afield/graphics/paint/gestureReplay.kt — read the source

Top-level functions

Declaration Summary
fun PaintCanvas.gesture(samples: List<PenSample>, xRange: Pair<Double, Double> = -1.0 to 1.0, yRange: Pair<Double, Double> = -1.0 to 1.0, into: Rect? = null, pressure: Double = 0.6, pressureScale: Double = 1.2, tilt: Double = 30.0, lean: Boolean = true, timeScale: Double = 1.0): TimedGesture The take samples, recorded on a pad spanning xRange by yRange (y up, as the pad shows it), as a TimedGesture on this canvas: the pad's square laid over into (in this canvas's FLine coordinates, y down; by default the largest square in the middle 80% of the canvas), its timing kept and stretched by timeScale.
fun PaintCanvas.gesture(pad: GestureHandle, into: Rect? = null, pressure: Double = 0.6, pressureScale: Double = 1.2, tilt: Double = 30.0, lean: Boolean = true, timeScale: Double = 1.0): TimedGesture The take recorded on pad as a TimedGesture on this canvas; see the List<PenSample> overload. A pad made with pen = true brings its pressure, tilt and twist along.
fun PaintCanvas.gesture(x: KeyframeEvaluator, y: KeyframeEvaluator, xRange: Pair<Double, Double> = -1.0 to 1.0, yRange: Pair<Double, Double> = -1.0 to 1.0, into: Rect? = null, pressureLane: KeyframeEvaluator? = null, tiltXLane: KeyframeEvaluator? = null, tiltYLane: KeyframeEvaluator? = null, twistLane: KeyframeEvaluator? = null, pressure: Double = 0.6, pressureScale: Double = 1.2, tilt: Double = 30.0, lean: Boolean = true, timeScale: Double = 1.0, samplesPerSecond: Double = 60.0): TimedGesture Keyframe lanes x and y — a take baked with bakeGesture and edited in the keyframe editor — as a TimedGesture on this canvas, sampled samplesPerSecond times a second and at every key. pressureLane (pen pressure, 0 to 1, as GestureHandle.penLane("pressure") bakes it) presses the brush where it is given; otherwise pressure does, throughout. tiltXLane, tiltYLane and twistLane (degrees, penLane("tiltX") and so on) hold the handle as the pen was held. The rest is as in the List<PenSample> overload.
fun penPose(tiltX: Double, tiltY: Double): Pair<Double, Double> A pen's PointerEvent tiltX/tiltY (degrees toward +x and toward +y) as a TimedGesture.Sample's tilt from upright and the azimuth it leans toward, both degrees. Upright leans toward 0.

AccelerationStructure.kt

Vulkan ray-tracing acceleration structures: AccelerationStructure builds a bottom-level structure from triangles or AABBs, or a top-level one from RayTracingInstances, on the Renderer found from context.

Geometry is described by AccelerationStructureTriangles and AccelerationStructureAabbs; identityTransform3x4 is the default instance transform. After a build, accelerationStructureBuildBarrier makes it visible to later commands that read it, and rayTracingMemoryBarrier orders ray-tracing shader writes before later shader reads.

src/jvmMain/kotlin/afield/graphics/render/AccelerationStructure.kt — read the source

Used in: Ray Tracing Examples, Compute your ray-traced geometry

Top-level functions

Declaration Summary
fun identityTransform3x4(): FloatArray
fun accelerationStructureBuildBarrier(command: CommandBuffer)
fun rayTracingMemoryBarrier(command: CommandBuffer)

AccelerationStructureAddress

interface AccelerationStructureAddress

A BLAS device address usable in a TLAS instance, including NV cluster BLAS without a Vk handle.

HasAccelerationStructure

interface HasAccelerationStructure : AccelerationStructureAddress

Declaration Summary
val vkAccelerationStructure: Long

AccelerationStructureTriangles

data class AccelerationStructureTriangles(val vertexBuffer: HasBuffer, val vertexFormat: Int = VK_FORMAT_R32G32B32_SFLOAT, val vertexStride: Long = 12L, val maxVertex: Int, val primitiveCount: Int, val indexBuffer: HasBuffer? = null, val indexType: Int = VK_INDEX_TYPE_UINT32, val transformBuffer: HasBuffer? = null, val flags: Int = VK_GEOMETRY_OPAQUE_BIT_KHR, val primitiveOffset: Int = 0, val firstVertex: Int = 0, val transformOffset: Int = 0)

Declaration Summary
val vertexBuffer: HasBuffer
val vertexFormat: Int
val vertexStride: Long
val maxVertex: Int
val primitiveCount: Int
val indexBuffer: HasBuffer?
val indexType: Int
val transformBuffer: HasBuffer?
val flags: Int
val primitiveOffset: Int
val firstVertex: Int
val transformOffset: Int

AccelerationStructureAabbs

data class AccelerationStructureAabbs(val aabbBuffer: HasBuffer, val stride: Long = 24L, val primitiveCount: Int, val flags: Int = VK_GEOMETRY_OPAQUE_BIT_KHR, val primitiveOffset: Int = 0, val firstVertex: Int = 0, val transformOffset: Int = 0)

Declaration Summary
val aabbBuffer: HasBuffer
val stride: Long
val primitiveCount: Int
val flags: Int
val primitiveOffset: Int
val firstVertex: Int
val transformOffset: Int

RayTracingInstance

data class RayTracingInstance(val accelerationStructure: AccelerationStructureAddress, val transform: FloatArray = identityTransform3x4(), val customIndex: Int = 0, val mask: Int = 0xff, val shaderBindingTableRecordOffset: Int = 0, val flags: Int = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR)

Declaration Summary
val accelerationStructure: AccelerationStructureAddress
val transform: FloatArray
val customIndex: Int
val mask: Int
val shaderBindingTableRecordOffset: Int
val flags: Int

AccelerationStructure

class AccelerationStructure(val renderer: Renderer = fromContext(), private val debugName: String = "acceleration-structure") : HasAccelerationStructure

Declaration Summary
val renderer: Renderer
override var vkAccelerationStructure: Long
override fun deviceAddress(): Long
fun buildBottomLevel(command: CommandBuffer, triangles: List<AccelerationStructureTriangles> = emptyList(), aabbs: List<AccelerationStructureAabbs> = emptyList(), flags: Int = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR): AccelerationStructure
fun buildTopLevel(command: CommandBuffer, instances: List<RayTracingInstance>, flags: Int = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR): AccelerationStructure
fun buildTopLevel(command: CommandBuffer, instances: VulkanBuffer, instanceCount: Int, flags: Int = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR): AccelerationStructure GPU-produced instance records, on a fresh owner. The first call fixes count and flags: recreate the TLAS to change them, retiring the old owner after GPU completion. This avoids resizing its storage/scratch while previous frames still reference them. Update transforms and addresses within those fixed records freely. Caller owns and synchronizes the producer buffer on Field's graphics queue. No host mapping or per-frame upload occurs.
fun buildBottomLevelNow(triangles: List<AccelerationStructureTriangles> = emptyList(), aabbs: List<AccelerationStructureAabbs> = emptyList(), flags: Int = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR): AccelerationStructure
fun buildTopLevelNow(instances: List<RayTracingInstance>, flags: Int = VK_BUILD_ACCELERATION_STRUCTURE_PREFER_FAST_TRACE_BIT_KHR): AccelerationStructure
fun cleanup()

AdvancedVulkanFallbackSmoke.kt

src/jvmMain/kotlin/afield/graphics/render/AdvancedVulkanFallbackSmoke.kt — read the source

AdvancedVulkanFallbackSmoke

object AdvancedVulkanFallbackSmoke

Force optional feature gates off on a supporting device and exercise ordinary shader inference.

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

ClusterGeometry.kt

Builds NVIDIA cluster acceleration structures from compute-shader output: ClusterGeometry validates TriangleCluster records, builds cluster-level acceleration structures and one cluster BLAS for ray tracing.

It requires VK_NV_cluster_acceleration_structure; ClusterGeometryInput names the producer buffers it reads.

src/jvmMain/kotlin/afield/graphics/render/ClusterGeometry.kt — read the source

Used in: Compute your ray-traced geometry

TriangleCluster

data class TriangleCluster(val firstVertex: Int,val firstIndex: Int,val vertexCount: Int,val triangleCount: Int, val clusterId: Int,val geometryId: Int=0)

One Field cluster description, eight uint32 words / 32 bytes, usable by ordinary compute shaders. Positions are vec4 (xyz in world/object units); indices are uint32 LOCAL to this cluster's vertex slice. A cluster may change counts, positions and indices on every build. IDs stay yours. Invalid GPU records become degenerate geometry and set ClusterGeometry.status rather than passing unchecked addresses/counts to Vulkan. No host readback occurs during builds.

Declaration Summary
val clusterId: Int,val geometryId: Int
fun write(out: ByteBuffer)

ClusterGeometryInput

data class ClusterGeometryInput(val positions: VulkanBuffer,val indices: VulkanBuffer,val clusters: VulkanBuffer)

Borrowed producer buffers. Keep alive through build completion; synchronize any subsequent writes. clusters contains TriangleCluster records. Buffer sizes, not arbitrary user-supplied addresses, determine bounds in the GPU validation/conversion pass.

ClusterGeometry

class ClusterGeometry(val renderer: Renderer,val capacity: Int,val verticesPerCluster: Int=128, val trianglesPerCluster: Int=128,val maxGeometryId: Int=0) : AccelerationStructureAddress,AutoCloseable

General NV Mega Geometry builder: Field compute -> validated clusters -> CLAS -> one cluster BLAS. It owns bounded build/storage/scratch resources, never producer buffers. Single graphics queue; call build explicitly after a producer changes geometry. Static geometry needs only one build. Recreate to increase capacity. A rebuild changes this BLAS's bounds: rebuild the containing TLAS. Cluster BLAS has an address but no VkAccelerationStructure handle; use it in RayTracingInstance.

Declaration Summary
val trianglesPerCluster: Int
data class Limits(val vertices: Int,val triangles: Int,val scratchAlignment: Int,val clusterAlignment: Int, val blasAlignment: Int,val maxGeometryId: Int)
val limits: Limits
val status: VulkanBuffer One uint per cluster: 0 valid, 1 invalid descriptor, 2 invalid index, 4 nonfinite vertex.
fun build(command: CommandBuffer,pipeline: RayTracingModule,input: ClusterGeometryInput,count: Int=capacity) Record outside a render pass on the graphics queue. The pipeline must opt into cluster AS; Vulkan requires it bound during cluster builds. Field rebinds descriptors/state when tracing.
override fun deviceAddress(): Long
override fun close()

ClusterGeometryDemo.kt

A computed-terrain demo of ClusterGeometry: a compute shader writes every patch's vertices, indices and cluster descriptions, and a ray-tracing pass renders the resulting cluster acceleration structure.

It needs a device with VK_NV_cluster_acceleration_structure.

src/jvmMain/kotlin/afield/graphics/render/ClusterGeometryDemo.kt — read the source

ClusterGeometryDemo

class ClusterGeometryDemo(val renderer: Renderer, val columns: Int = 32, val rows: Int = 24, val width: Int = 1920, val height: Int = 1080, val name: String = "computed-clusters", producerFactory: (Parameters) -> ComputeModulePointer = { ComputeModule(it).pointer() }) : Target, AutoCloseable

Computed terrain, using exactly the public ClusterGeometry input contract. The producer shader owns all vertices, indices and descriptions, and is replaceable through ordinary live reload. Each patch has 8x8 cells; topology switches diagonals without changing allocations.

Declaration Summary
val renderer: Renderer
val columns: Int
val rows: Int
val width: Int
val height: Int
val name: String
val count
val geometry
val positions
val indices
val descriptions
val producer
val output
val gpuMilliseconds
var time
var animate
var topology
var view
var invalidCluster
var invalidMode
fun install(): ClusterGeometryDemo
override fun render(command: CommandBuffer, index: Int)
override fun close()

ClusterGeometrySmoke.kt

src/jvmMain/kotlin/afield/graphics/render/ClusterGeometrySmoke.kt — read the source

ClusterGeometrySmoke

object ClusterGeometrySmoke

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

ComputeMesh.kt

ComputeMesh is a compute dispatch with its own push constants inside a ComputeModule; the rest of the file is the barriers that order compute work against transfers, graphics and host reads.

Any ComputeTask can be added to a module, and computeTask wraps a lambda as one. computeBarrier and zeroBuffer are ready-made tasks; computeMemoryBarrier, graphicsMemoryBarrier, transferMemoryBarrier, hostReadBarrier and graphicsHostReadBarrier record a Vulkan synchronization-2 barrier into a command buffer.

src/jvmMain/kotlin/afield/graphics/render/ComputeMesh.kt — read the source

Top-level functions

Declaration Summary
fun computeTask(c: (inside: ComputeModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer) -> Unit): ComputeTask
fun computeBarrier(graphics: Boolean = false, buffer: VulkanBuffer? = null): ComputeTask Publish preceding compute shader writes to subsequent compute work on the same queue. With graphics true, also publish to graphics shader reads/writes. Optional buffer limits the dependency to that buffer; null is a global memory dependency. This does not transfer ownership between queues and does not make results visible to host reads.
fun zeroBuffer(v: VulkanBuffer, barrier: Boolean = true)
fun transferMemoryBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null) TRANSFER->SHADER-READ (including task/mesh, fragment and compute). Falls back to a MemoryBarrier2 if buffer==null.
fun computeMemoryBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null) COMPUTE write -> COMPUTE read / write.
fun graphicsMemoryBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null) COMPUTE write -> graphics shader read / write.
fun computeMemoryBarrier(cmd: CommandBuffer, buffer: Long? = null, size: Long = VK_WHOLE_SIZE)
fun graphicsMemoryBarrier(cmd: CommandBuffer, buffer: Long? = null, size: Long = VK_WHOLE_SIZE)
fun hostReadBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null) COMPUTE write -> HOST read.
fun graphicsHostReadBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null) GRAPHICS write -> HOST read.

ComputeTask

interface ComputeTask

Declaration Summary
fun compute(inside: ComputeModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer)

ComputeMesh

class ComputeMesh(var renderer: Renderer = fromContext()) : ComputeTask

Declaration Summary
var indirect: afield.graphics.vulkan.buffers.FrameBufferGroup? Optional GPU-written VkDispatchIndirectCommand; no CPU count readback.
var indirectOffset: Long
class FromUniform
class BoundFromStructure<T>
lateinit var inside: ComputeModulePointer
var update Named sequence hooks advanced once per compute call, before push constants and dispatch.
var push Push-constant values and bindings written to the buffer before each dispatch.
var x
var y
var z
class FromStructure<T>
override fun compute(inside: ComputeModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer)

ComputeModule.kt

A compute pipeline as a renderable module: ComputeModule compiles and introspects a compute shader, owns its descriptor sets, and dispatches the ComputeTasks registered in its meshes.

Boxes hold it through a ComputeModulePointer, which swaps in a rebuilt module when the shader is reloaded and carries live values across the interface change.

src/jvmMain/kotlin/afield/graphics/render/ComputeModule.kt — read the source

Top-level functions

Declaration Summary
fun ComputeModule.pointer()

ComputeModulePointer

class ComputeModulePointer(var current: ComputeModule) : Renderable

Declaration Summary
var lastReloadReport: ModuleReloadReport? What the last successful reload did to the interface and which values it could not carry.
override fun render(command: CommandBuffer, bounds: Rectanglef)
enum class ReloadOutcome What a reloadOutcome did.
fun reload(p: Parameters, onError: (String, Throwable?) -> Unit = p.errorHandler): Boolean True when the module was replaced. False covers both "already compiled" and "did not compile"; reloadOutcome tells them apart.
fun reloadOutcome(p: Parameters, onError: (String, Throwable?) -> Unit = p.errorHandler): ReloadOutcome reload, saying which of the three things happened.
operator fun plusAssign(m: Pair<String, BeforeTarget>)+2 overloads · also Pair<String, ComputeTask>, Pair<String, Target>
fun zeroBefore(name: String, buffer: FrameBufferGroup)+1 overload · also VulkanBuffer
fun enable()
fun disable()

ComputeModule

class ComputeModule(val parameters: Parameters) : Renderable

Declaration Summary
var valid
var enabled
val meshes
val before
val zeroBefore
val after
val ci
var csm: Long
val pushConstantSize
var uniforms: MutableMap<String, UpdatesDescriptorSet>
var liveUniformStructures: MutableMap<String, Structure>
var pipelineLayout: Long
var pipeline: Long
data class PipelineUpdate(val computeIntrospection: Introspection, val shaderModule: Long, val pipeline: Long)
fun buildPipelineUpdate(newCompute: Introspection): PipelineUpdate
fun installPipelineUpdate(update: PipelineUpdate)
fun destroyUnusedPipelineUpdate(update: PipelineUpdate)
override fun render(command: CommandBuffer, bounds: Rectanglef)
fun enable()
fun disable()
fun copyMeshesFrom(old: ComputeModule)
fun cleanup() Release this module's pipeline, layout, shader module and descriptor layouts. It stops dispatching at once; while its renderer is alive the handles go through the retirement queue, as for Module.cleanup, and at once after the renderer has closed.
fun zeroBefore(name: String, buffer: FrameBufferGroup)+1 overload · also VulkanBuffer

FLineUpload.kt

Writes FLines into GPU vertex streams: pointsOut, linesOut and trianglesOut sample lines into DynamicallyWriteable position and colour buffers.

FLineMap is a named, ordered collection of lines with a revision hash for change detection.

src/jvmMain/kotlin/afield/graphics/render/FLineUpload.kt — read the source

Top-level functions

Declaration Summary
fun pointsOut(m: FLineMap, vertexData: DynamicallyWriteable, pointData: DynamicallyWriteable, colorData: DynamicallyWriteable) Convenience overload of pointsOut for an FLineMap.
fun pointsOut(m: Collection<FLine>, vertexData: DynamicallyWriteable, colorData: DynamicallyWriteable, pointSizeData: DynamicallyWriteable): Int Write point positions, colours and sizes for a collection of FLines.
fun trianglesOut(m: FLineMap, vertexData: DynamicallyWriteable, colorData: DynamicallyWriteable, samplesPerCubic: Int = 100) Convenience overload of trianglesOut for an FLineMap.
fun trianglesOut(m: Collection<FLine>, vertexData: DynamicallyWriteable, colorData: DynamicallyWriteable, samplesPerCubic: Int = 100): Int Tessellate filled FLines into triangles and write positions and colors.
fun linesOut(m: FLine, vertexData: DynamicallyWriteable? = null, colorData: DynamicallyWriteable? = null, samplesPerCubic: Int = 100) Convenience overload of linesOut for a single FLine.
fun linesOut(m: FLineMap, vertexData: DynamicallyWriteable? = null, colorData: DynamicallyWriteable? = null, samplesPerCubic: Int = 100) Convenience overload of linesOut for an FLineMap.
fun linesOut(m: Collection<FLine>, vertexData: DynamicallyWriteable? = null, colorData: DynamicallyWriteable? = null, samplesPerCubic: Int = 100): Int Linearize stroke paths into a vertex stream, plus an optional colour stream indexed by node.

FLineMap

class FLineMap : LinkedHashMap<String, FLine>()

Ordered map of named FLines with a lightweight revision hash.

Declaration Summary
val rev: Long Combined revision hash of all contained lines.
operator fun plusAssign(f: Pair<String, FLine>)+2 overloads · also FLine, Named<FLine>
override fun toString(): String

FanOutDispatch.kt

src/jvmMain/kotlin/afield/graphics/render/FanOutDispatch.kt — read the source

FourVisualizers.kt

Inline value printers for the Vulkan graphics types, so a renderer, buffer or texture shows as a readable table when a box prints it.

registerFourVisualizers runs at each new root and registers a printer for Renderer, Camera, Window, the device and swapchain types, images and buffers, uniform Structures, the module and module-pointer types, Mesh, render-to-texture targets, LoadedTexture and StorageImage. Each printer renders a small coloured table of the object's state: frame counter and upload bytes for a renderer, extents and formats for images, field values for a structure. TexturePreviewCache holds the downscaled previews shown beside texture values.

src/jvmMain/kotlin/afield/graphics/render/FourVisualizers.kt — read the source

Top-level functions

Declaration Summary
fun registerFourVisualizers()

TexturePreviewCache

object TexturePreviewCache

Thread-safe cache of downsampled texture previews keyed by VkImage handle. Call invalidate when an image is destroyed or recreated.

Declaration Summary
fun invalidate(vkImage: Long)
fun clear()
fun getCachedImgTag(vkImage: Long): String? Return cached <img> tag HTML, or null.
fun capture(renderer: Renderer, vkImage: Long, format: Int, srcWidth: Int, srcHeight: Int, currentLayout: Int, maxDim: Int = MAX_DIM): String? Synchronous one-shot GPU readback of vkImage into a small PNG preview.

GeometryAgentTools.kt

A worked, public example of giving Field's AI its own tools — two of them, over the geometry the loaders in MeshFromDisk.kt read off disk.

What this is for

afield.ai.tools.AgentToolExtensions is how any code outside afield.ai.core adds tools to AI4. This file is the runnable half of public/docs/aiFeatures.md's "Give the AI a tool of your own", built out of parts that are useful on their own: meshFromAnyFile reads OBJ, STL, PLY, glTF/GLB and everything else Assimp handles, and afield.geometry.mesh.renderPng rasterises the result with no GPU, no window and no AWT.

That last property is what makes the second tool worth copying. A model asked "is the scan I dropped in there the right way up?" can look, because InspectGeometryFileResult implements CarriesToolImages and the picture travels as an image rather than as base64 spent in a JSON string.

Using it

// in a box:
GeometryAgentTools.addSearchRoot(File(System.getProperty("user.home"), "scans"))
GeometryAgentTools.register()
// then open AI Chat and ask about the files in there.

GeometryAgentTools.register is idempotent, so leaving those two lines in a box you re-run is fine, and a re-run reaches a conversation that is already open.

The three things this example is trying to demonstrate

  1. A gate on the tool set. Both tools are unavailable until a search root is added. A tool set that is always on costs its schema on every request in every conversation, including the ones that have nothing to do with it.
  2. A result that summarises itself. Both results implement SummarisesForToolLog, so the tool I/O log — which AiToolUseImprovementMonitor reads — records what happened rather than a class name.
  3. Refusing to read outside the roots. These tools take a path from a language model. The roots are the entire permission model, and resolveInsideRoots is where it is enforced; a symlink or a ../ that escapes is an error, not a read. Every tool that takes a path from a model needs an equivalent, and it is the part an example is most likely to leave out.

What this does not do

It does not modify anything. There is no ConvertGeometryFile here: a writing tool needs a confirmation protocol (the personal house cluster's commit=false preview pattern), which is a bigger subject than one example should carry.

src/jvmMain/kotlin/afield/graphics/render/GeometryAgentTools.kt — read the source

Used in: Geometry from disk

GeometryAgentTools

object GeometryAgentTools

Registers the geometry inspection tools with AI4 once a search root has been added.

Declaration Summary
val LOADABLE_EXTENSIONS: Set<String> Extensions meshFromAnyFile can open, lower-cased and without the dot.
fun addSearchRoot(dir: File) Let the AI see geometry files under dir. Nothing outside a root added here is readable.
fun clearSearchRoots() Take every root away, which also makes enabled false. Symmetric with addSearchRoot, and for tests.
fun searchRoots(): List<File> The roots, canonicalised, in the order they were added.
fun enabled(): Boolean Whether there is anywhere to look at all. Both tools check this before they run.
fun register() Register both tools with AI4.
fun unregister() Retire both tools. A conversation that is open loses them on its next turn.

ListGeometryFiles

data class ListGeometryFiles(@JsonPropertyDescription("Only list files whose name contains this text, case-insensitively. Omit for all of them.") val nameContains: String = "", @JsonPropertyDescription("At most this many files, 1..500. Defaults to 40.") val limit: Int = 40)

Tool 1: what is there.

Declaration Summary
val nameContains: String
val limit: Int

GeometryFileEntry

data class GeometryFileEntry(val path: String, val root: String, val format: String, val bytes: Long)

Declaration Summary
val path: String
val root: String
val format: String
val bytes: Long

ListGeometryFilesResult

data class ListGeometryFilesResult(val files: List<GeometryFileEntry>, val totalMatches: Int, val truncated: Boolean, val searchRoots: List<String>) : SummarisesForToolLog

Declaration Summary
val files: List<GeometryFileEntry>
val totalMatches: Int
val truncated: Boolean
val searchRoots: List<String>
override fun toolLogSummary(): String

InspectGeometryFile

data class InspectGeometryFile(@JsonPropertyDescription("The file to inspect: a path from ListGeometryFiles, or an absolute path inside a search root.") val path: String, @JsonPropertyDescription("Return a rendered image of the mesh. True by default; set false when only the numbers are wanted.") val render: Boolean = true, @JsonPropertyDescription("Camera orbit around the vertical axis in degrees. Ask again with a different value to see another side.") val yawDegrees: Double = 28.0, @JsonPropertyDescription("Camera height above the horizon in degrees. Positive looks down on the object.") val pitchDegrees: Double = 18.0, @JsonPropertyDescription("Draw the triangle edges over the shading. Useful for judging mesh density; illegible above about 50,000 triangles.") val wireframe: Boolean = false)

Tool 2: what one of them is, and what it looks like.

Declaration Summary
val path: String
val render: Boolean
val yawDegrees: Double
val pitchDegrees: Double
val wireframe: Boolean

InspectGeometryFileResult

data class InspectGeometryFileResult(val path: String, val root: String, val format: String, val bytesOnDisk: Long, val vertexCount: Int, val triangleCount: Int, val hasNormals: Boolean, val hasUvs: Boolean, val hasColors: Boolean, val extent: List<Double>, val minCorner: List<Double>, val surfaceArea: Double, val volume: Double?, val watertight: Boolean, val eulerCharacteristic: Int, val boundaryLoopCount: Int, val notes: List<String>, override val toolImages: List<ToolImage> = emptyList()) : SummarisesForToolLog, CarriesToolImages

What a model gets back — and, separately, what it gets to see.

Declaration Summary
val path: String Relative to root, which is the form ListGeometryFiles reports and accepts back.
val root: String
val format: String
val bytesOnDisk: Long
val vertexCount: Int
val triangleCount: Int
val hasNormals: Boolean
val hasUvs: Boolean
val hasColors: Boolean
val extent: List<Double>
val minCorner: List<Double>
val surfaceArea: Double
val volume: Double? Null unless the mesh is watertight — a volume for an open shell is a number with no meaning.
val watertight: Boolean
val eulerCharacteristic: Int
val boundaryLoopCount: Int
val notes: List<String>
override val toolImages: List<ToolImage>
override fun toolLogSummary(): String

Mesh.kt

Mesh is the vertex-buffer draw call of a graphics module: it holds the vertex inputs and push constants the module's shaders declare, and draws them on the Renderer found from context.

Its layout has to agree with the vertex-buffer structure the module's pipeline was built with. fullscreenTriangle makes the one-triangle mesh used for full-screen passes.

src/jvmMain/kotlin/afield/graphics/render/Mesh.kt — read the source

Used in: Field 4 Graphics, Field Probes: instrumenting shaders, FieldML's JVM bridge

Top-level functions

Declaration Summary
fun fullscreenTriangle(module: ModulePointer = fromContext()): Mesh Three non-indexed vertices for a fullscreen shader that derives positions from gl_VertexIndex. module defaults to the current box's graphics module. No vertex attributes are allocated and nothing is attached or presented: add the returned mesh to the module under your chosen name.

Top-level values

Declaration Summary
val restart: Int The primitive-restart index: write it into a strip or fan index buffer to end the current run and begin another. Meshes index with VK_INDEX_TYPE_UINT32, so the value is all thirty-two bits set, and a module's pipeline turns restart on for exactly the strip and fan topologies.

FromInput

class FromInput

Declaration Summary
operator fun getValue(inside: Mesh, property: KProperty<*>): VertexBuffer

Mesh

class Mesh(var renderer: Renderer = fromContext())

Declaration Summary
var renderer: Renderer The renderer the vertex and index buffers are allocated on, and whose frame counter decides when prepareForDraw has already run for this frame.
lateinit var inside: ModulePointer Module pointer supplying the mesh's shader inputs and push-constant layout.
var v The vertex buffers by the shader input location they feed. resize creates one per input the module declares, grows or trims it to the vertex count, and frees any whose input the current shaders do not declare.
var vn The same buffers again, by input name — the map input reads, and the one to reach for from box code, where the shader's position is easier to name than its location.
var t: VertexBuffer? The index buffer, or null when the mesh is not indexed. resize allocates it only for a non-zero triangle count, and drops it again when that count goes back to zero.
var numTriangle: Int Primitives the last resize made room for, or -1 before the first one. Above zero it is an indexed draw of this many primitives; at zero the mesh draws numVert vertices straight through.
var numVert: Int Vertices the last resize made room for, or -1 before the first one.
var update Named sequences advanced once per frame, from prepareForDraw, before the buffers are flushed — which is where a mesh that rewrites its own vertices each frame puts that work.
var instanceCount How many copies of the mesh each draw call asks for; reaches vkCmdDraw and vkCmdDrawIndexed directly, so the shader distinguishes them by gl_InstanceIndex.
var push The push constants for this draw, laid out to match the vertex and fragment members the current module declares. The FromStructure delegates read and write it by member name.
class BoundFromStructure<T>
class FromStructure<T>
fun input(name: String): VertexBuffer Returns the vertex buffer for the named mesh input.
operator fun get(name: String)
var notInitialized Whether resize has yet run. Only input reads it, to tell "there is no such input in this module" from "there are no inputs yet because nothing has been sized".
var builder The open-ended writer for this mesh's vertices and indices. prepareForDraw closes and applies it if the builder { } block left it open, so the contents land in the buffers before the draw that needs them.
fun builder(b: MeshBuilder.() -> Unit): MeshBuilder
fun primitiveSize(): Int
fun resize(numVert: Int, numTriangle: Int = 0, inside: ModulePointer = this.inside): Mesh here numTriangle = 0 means 'we're not indexed and we don't need an index buffer'
fun write(index: Int, numElements: Int, builder: FloatBuffer.() -> Unit)
fun write(name: String, inside: Module = this.inside.current, builder: FloatBuffer.() -> Unit)
fun prepareForDraw(cmd: CommandBuffer)
fun draw(inside: Module, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer)
override fun toString(): String

MeshBuilder.kt

Two ways of writing vertices into a Mesh, of which one is wired up.

writeSuperMap is the live one, and it is where m.set(4, 2) { … } in a shader box comes from. It reads a vertex shader's introspected inputs and returns Kotlin source: a MappedStructure holding one buffer field per input, and a Mesh.set(numVert, numTriangle) extension that resizes the mesh and maps all of those buffers at once, so the lambda can assign them by the names the shader itself declares. insertForShader calls this whenever a moduleHere box's vertex and fragment shaders compile, and appends the text to that box's preamble.

MeshBuilder, VertexData and writeVertexData are the other way: stage vertex bytes on the CPU through generated write(...) calls, then copy them into the mesh's buffers in one pass. Mesh constructs a MeshBuilder for every mesh and offers it as builder { }, but nothing in the tree calls writeVertexData to generate those write methods, and nothing calls the internal closeAndApply that copies the staged bytes across, so vertices staged this way never reach the mesh. m.set is the route to use.

src/jvmMain/kotlin/afield/graphics/render/MeshBuilder.kt — read the source

Top-level functions

Declaration Summary
fun writeSuperMap(v: List<Introspected.Input>): String The MappedStructure and Mesh.set source text for a vertex shader's inputs v, as one line.
fun writeVertexData(n: List<Introspected.Input>): String The MeshBuilder.write(...) source text for a vertex shader's inputs n: one parameter per input, defaulted, writing into that input's own VertexData.

VertexData

class VertexData

A growable byte buffer for one vertex attribute, with the number of vertices written to it.

Declaration Summary
var cap
var buffer
var v
fun rewind()
fun checkCapacity(n: Int)

MeshBuilder

class MeshBuilder(val target: Mesh)

Stages one mesh's vertices and triangles in host memory, with a slot for each of sixteen shader input locations.

Declaration Summary
val data
val triangle
var isOpen
fun tri(a: Int, b: Int, c: Int)
fun tri()
fun quad()
fun getDataAndCheckCapacity(layout: Int, sizeInBytes: Int): VertexData

MeshMesh.kt

The MeshTask interface for issuing mesh-shader work, and MeshMesh, a task whose push constants and uniforms are bound to Kotlin properties by name.

meshTask wraps a lambda as a MeshTask.

src/jvmMain/kotlin/afield/graphics/render/MeshMesh.kt — read the source

Top-level functions

Declaration Summary
fun meshTask(c: (inside: MeshModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer) -> Unit): MeshTask

MeshTask

interface MeshTask

Declaration Summary
fun compute(inside: MeshModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer)

MeshMesh

class MeshMesh(var renderer: Renderer = fromContext()) : MeshTask

Declaration Summary
var indirect: FrameBufferGroup? GPU-written VkDrawMeshTasksIndirectCommandEXT; null keeps direct x/y/z dispatch.
var indirectOffset: Long Byte offset of the three uint group counts, aligned to four bytes.
fun prepareForDraw(command: CommandBuffer) Transfers must be prepared outside dynamic rendering, like a conventional mesh's buffers.
class FromUniform
class BoundFromStructure<T>
lateinit var inside: MeshModulePointer
var update
var push
var x
var y
var z
class FromStructure<T>
override fun compute(inside: MeshModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer)

MeshModule.kt

A task/mesh-shader pipeline as a renderable module: MeshModule compiles and introspects task and mesh shaders and owns their descriptor sets.

Boxes hold it through a MeshModulePointer, which swaps in a rebuilt module when a shader is reloaded and carries live values across the interface change.

src/jvmMain/kotlin/afield/graphics/render/MeshModule.kt — read the source

Top-level functions

Declaration Summary
fun MeshModule.pointer()

MeshModulePointer

class MeshModulePointer(var current: MeshModule) : Renderable

Declaration Summary
var lastReloadReport: ModuleReloadReport? What the last successful reload did to the interface and which values it could not carry.
fun prepareSampledImagesFor(command: CommandBuffer)
override fun render(command: CommandBuffer, bounds: Rectanglef)
fun reload(p: Parameters, forceReresolve: Boolean = true, onError: (String, Throwable?) -> Unit = p.errorHandler): Boolean
operator fun plusAssign(m: Pair<String, BeforeTarget>)+3 overloads · also Pair<String, MeshMesh>, Named<MeshMesh>, Pair<String, Target>
fun enable()
fun disable()
override fun toString(): String

MeshModule

class MeshModule(val parameters: Parameters) : Renderable

Declaration Summary
var valid
var enabled
var meshes
var before
var after
val ti
val mi
val fi
val pushConstantSize
var uniforms: MutableMap<String, UpdatesDescriptorSet>
var liveUniformStructures: MutableMap<String, Structure>
var pipelineLayout: Long
var pipeline: Long
var tsm: Long
var msm: Long
var fsm: Long
override fun render(command: CommandBuffer, bounds: Rectanglef)
fun enable()
fun disable()
fun copyMeshesFrom(old: MeshModule)
fun cleanup() Release this module's pipeline, layout, shader modules and descriptor layouts. It stops drawing at once; while its renderer is alive the handles go through the retirement queue, as for Module.cleanup — a full reload retires the module it replaces while frames that recorded it may still be in flight — and at once after the renderer has closed.
operator fun plusAssign(m: Pair<String, BeforeTarget>)+2 overloads · also Pair<String, MeshMesh>, Pair<String, Target>

Module.kt

The rasterising Module: one graphics pipeline built from vertex/fragment (or task/mesh) GLSL, the Meshes it draws, and the uniforms, textures and push constants bound to it.

Parameters carries the shader sources and pipeline settings, and parameters builds one against the renderer found by fromContext(). A ModulePointer stands in front of a module so a box can recompile its shaders and swap in the new Module without the renderer losing its reference; Module.pointer makes one. before wraps a command-buffer callback as a target that runs ahead of the module's draws, and parseCompilerException splits a shader compiler error into file, line and message.

Box code usually reaches this through moduleHere rather than constructing a Module directly; the graphics lessons in public/docs/assets/canonical_graphics/LESSONS.md show that shape.

src/jvmMain/kotlin/afield/graphics/render/Module.kt — read the source

Used in: Fabricate Renderer, FieldML's JVM bridge, A solid, written down: afield.lang.solidsl

Top-level functions

Declaration Summary
fun attachDeclaredMeshes(module: ModulePointer, p: Parameters) Creates each mesh declared in p's block, adds it to module under its name, then runs its build block, in declaration order. Re-running the box replaces a mesh of the same name.
fun parameters(renderer: Renderer = fromContext(), build: Parameters.() -> Unit): Parameters
fun Module.pointer()
fun before(a: (CommandBuffer, Int) -> Unit)
fun parseCompilerException(exception: CompilerError, accept: (String, Int, String) -> Unit)

Parameters

data class Parameters(val renderer: Renderer, var vertex: String = "", var fragment: String = "", var compute: String = "", var mesh: String = "", var task: String = "", var raygen: String = "", var miss: String = "", var closesthit: String = "", var anyhit: String = "", var intersection: String = "", var callable: String = "", var vertexIntrospection: Introspection? = null, var fragmentIntrospection: Introspection? = null, var computeIntrospection: Introspection? = null, var meshIntrospection: Introspection? = null, var taskIntrospection: Introspection? = null, var raygenIntrospection: Introspection? = null, var missIntrospection: Introspection? = null, var closesthitIntrospection: Introspection? = null, var anyhitIntrospection: Introspection? = null, var intersectionIntrospection: Introspection? = null, var callableIntrospection: Introspection? = null, var vertexSourceName: String = "vertex.glsl", var fragmentSourceName: String = "fragment.glsl", var computeSourceName: String = "compute.glsl", var meshSourceName: String = "mesh.glsl", var taskSourceName: String = "task.glsl", var raygenSourceName: String = "raygen.rgen", var missSourceName: String = "miss.rmiss", var closesthitSourceName: String = "closesthit.rchit", var anyhitSourceName: String = "anyhit.rahit", var intersectionSourceName: String = "intersection.rint", var callableSourceName: String = "callable.rcall", var vertexIncludeResolver: ShaderIncludeResolver? = null, var fragmentIncludeResolver: ShaderIncludeResolver? = null, var computeIncludeResolver: ShaderIncludeResolver? = null, var meshIncludeResolver: ShaderIncludeResolver? = null, var taskIncludeResolver: ShaderIncludeResolver? = null

Shader sources and pipeline settings shared by Module and RayTracingModule.

Declaration Summary
val renderer: Renderer
var vertex: String
var fragment: String
var compute: String
var mesh: String
var task: String
var raygen: String
var miss: String
var closesthit: String
var anyhit: String
var intersection: String
var callable: String
var vertexIntrospection: Introspection?
var fragmentIntrospection: Introspection?
var computeIntrospection: Introspection?
var meshIntrospection: Introspection?
var taskIntrospection: Introspection?
var raygenIntrospection: Introspection?
var missIntrospection: Introspection?
var closesthitIntrospection: Introspection?
var anyhitIntrospection: Introspection?
var intersectionIntrospection: Introspection?
var callableIntrospection: Introspection?
var vertexSourceName: String
var fragmentSourceName: String
var computeSourceName: String
var meshSourceName: String
var taskSourceName: String
var raygenSourceName: String
var missSourceName: String
var closesthitSourceName: String
var anyhitSourceName: String
var intersectionSourceName: String
var callableSourceName: String
var vertexIncludeResolver: ShaderIncludeResolver?
var fragmentIncludeResolver: ShaderIncludeResolver?
var computeIncludeResolver: ShaderIncludeResolver?
var meshIncludeResolver: ShaderIncludeResolver?
var taskIncludeResolver: ShaderIncludeResolver?
var raygenIncludeResolver: ShaderIncludeResolver?
var missIncludeResolver: ShaderIncludeResolver?
var closesthitIncludeResolver: ShaderIncludeResolver?
var anyhitIncludeResolver: ShaderIncludeResolver?
var intersectionIncludeResolver: ShaderIncludeResolver?
var callableIncludeResolver: ShaderIncludeResolver?
var color: Int
var colorFormats: List<Int>
var depth: Int
var depthFormat: Int
var primitiveType: Int
var textureResolver: (Introspected.Texture) -> Pair<HasImageViewArray, TextureSampler>?
var uboResolver: (BufferParameters) -> BufferParameters?
var ssboResolver: (BufferParameters) -> BufferParameters?
var ssboSizer: (Introspected.UBO) -> Int?
var imageResolver: (Introspected.Image) -> ImageView?
var accelerationStructureResolver: (Introspected.AccelerationStructure) -> HasAccelerationStructure?
var rayTracingGroups: RayTracingGroups?
var maxRayRecursionDepth: Int
var rayTracingPipelineFlags: Int
var allowClusterAccelerationStructure: Boolean Explicit pipeline opt-in for NV cluster geometry. Baseline pipelines need no extension.
var name: String
var errorHandler: (String, Throwable?) -> Unit
var colorMask: Array<Boolean>
var blend: Boolean
var srcColor: Int
var dstColor: Int
var colorBlendOp: Int
var srcAlpha: Int
var dstAlpha: Int
var alphaBlendOp: Int
var depthTest: Boolean
var depthWrite: Boolean
var depthCompareOp: Int
var viewport: Rect?
var viewportCount: Int
var viewMask: Int
var fillMode: Int
var imageFormat: Int
val labelColor: Vec4
var fieldProbe: ShaderFieldProbe?
var fieldStats: ShaderFieldStatsBinding?
val declaredMeshes Meshes declared with +Mesh("name") { ... } inside a moduleHere { } block. A mesh cannot be sized before its module exists, and this block runs first, so they wait here and the module builder creates, adds and fills them once it has built the module.
fun Mesh(name: String, build: Mesh.() -> Unit = {}): DeclaredMesh Inside moduleHere { }: a mesh called name for this module, filled by build once the module exists. Add it with a leading +: +Mesh("my mesh") { set(3, 3) { ... } }. Elsewhere Mesh() is the ordinary constructor.
operator fun DeclaredMesh.unaryPlus() Adds a Mesh declared in this block to the module.
val triangles
val lines
val points
val triangle_strip
val line_strip
val filled
val wireframe
val depth_never
val depth_less
val depth_equal
val depth_less_or_equal
val depth_greater
val depth_not_equal
val depth_greater_or_equal
val depth_always
val pipelineDepthFormat: Int The depth format the pipeline is built for: depthFormat when the module has a depth attachment (depth > 0), otherwise VK_FORMAT_UNDEFINED. depth = 0 alone leaves depthFormat at its default, and a pipeline declaring that format would not match a pass that supplies no depth view.
operator fun RenderToTexture.unaryPlus() Inside moduleHere { }: this module draws into this target. +target gives the module the target's colour formats, depth and viewports, which it needs before it can be attached with target += module.
operator fun HasRenderToTexture.unaryPlus() Inside moduleHere { }: this module draws into the target this object owns, e.g. +view for an ElectronRTTView.
operator fun MultiPingPongRenderToTexture.unaryPlus() Inside moduleHere { }: this module draws into this ping-pong target, as +target does for a RenderToTexture.
fun textures(vararg t: Pair<String, HasImageView>)
fun textureArrays(vararg t: Pair<String, HasImageViewArray>) Bind providers whose image views can change between frames (video ring buffers, ping-pong targets, and similar resources). Unlike textures, this keeps the provider itself live instead of capturing one of its current image views while the module is configured.

DeclaredMesh

class DeclaredMesh(val name: String, val build: Mesh.() -> Unit)

A mesh declared inside moduleHere { +Mesh("name") { ... } }; see Parameters.declaredMeshes.

Renderable

interface Renderable

Declaration Summary
fun render(command: CommandBuffer, bounds: Rectanglef)

FromUniform

class FromUniform

Declaration Summary
operator fun getValue(inside: ModulePointer, property: KProperty<*>): Structure

ModulePointer

class ModulePointer(var current: Module) : Renderable

Declaration Summary
var lastReloadReport: ModuleReloadReport? What the last successful reload did to the interface and which values it could not carry.
fun prepareSampledImagesFor(command: CommandBuffer)
override fun render(command: CommandBuffer, bounds: Rectanglef)
fun reload(p: Parameters, forceReresolve: Boolean = true, onError: (String, Throwable?) -> Unit = p.errorHandler): Boolean
operator fun plusAssign(m: Pair<String, BeforeTarget>)+5 overloads · also Pair<String, Mesh>, Named<Mesh>, Pair<String, Target>, Named<Target>, Named<BeforeTarget>
fun enable()
fun disable()
override fun toString(): String

Module

class Module(val parameters: Parameters) : Renderable

Declaration Summary
var valid
var enabled
var meshes
var before
var after
val vi
val fi
val namedInputs
val pushConstantSize
var uniforms: MutableMap<String, UpdatesDescriptorSet>
var liveUniformStructures: MutableMap<String, Structure>
class Uniform(val name: String, override val binding: Bindable, override var source: BufferParameters? = null, val buffer: FrameBufferGroup, val kind: Int = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER) : UpdatesDescriptorSet
class TexturePointer(val name: String, override val binding: Bindable, override var source: BufferParameters? = null, val texture: HasImageViewArray, val sampler: TextureSampler) : UpdatesDescriptorSet
class TextureArrayPointer(val name: String, override val binding: Bindable, override var source: BufferParameters? = null, val texture: HasImageViewArray, val sampler: TextureSampler) : UpdatesDescriptorSet
class ImagePointer(val name: String, override val binding: Bindable, override var source: BufferParameters? = null, val texture: HasImageView) : UpdatesDescriptorSet
lateinit var vertexInputAttributes: VkVertexInputAttributeDescription.Buffer
lateinit var vertexInputBindings: VkVertexInputBindingDescription.Buffer
lateinit var vertexInputState: VkPipelineVertexInputStateCreateInfo
var pipelineLayout: Long
var pipeline: Long
var vsm: Long
var fsm: Long
data class PipelineUpdate(val vertexIntrospection: Introspection, val fragmentIntrospection: Introspection, val vertexShaderModule: Long, val fragmentShaderModule: Long, val pipeline: Long)
fun buildPipelineUpdate(newVertex: Introspection, newFragment: Introspection): PipelineUpdate
fun installPipelineUpdate(update: PipelineUpdate)
fun destroyUnusedPipelineUpdate(update: PipelineUpdate)
override fun render(command: CommandBuffer, bounds: Rectanglef)
fun enable()
fun disable()
fun copyMeshesFrom(old: Module)
fun cleanup() Release this module's pipeline, pipeline layout, shader modules and descriptor layouts.
operator fun plusAssign(m: Pair<String, BeforeTarget>)+2 overloads · also Pair<String, Mesh>, Pair<String, Target>

BeforeTarget

value class BeforeTarget(val target: Target)

ModuleRebuild.kt

Told when a module pointer swaps in a rebuilt module (a full shader reload), with the old and new modules' live uniform structures, before the old one is dropped.

Order. The generic carry-over (ReloadCarryOver.finish) runs first and is authoritative: it has already moved every value and binding that still fits the new reflection, and dropped (and reported) the rest. Only then are hooks called, with a Rebuild saying which new structures were carried and from which old one. A hook fills in what the generic carry could not know (a stack.auto numbers block with no old counterpart is seeded from its slot) and must not re-apply what was carried, or it would restore bindings the carry dropped as retyped and values it refused. Both steps run in ReloadCarryOver.finishAndNotify.

An in-place pipeline update keeps its structures and does not come here.

src/jvmMain/kotlin/afield/graphics/render/ModuleRebuild.kt — read the source

ModuleRebuild

object ModuleRebuild

The registry of rebuild hooks, called after the generic carry-over on every full shader reload.

Declaration Summary
class Rebuild(val pointer: Any, val old: Map<String, Structure>, val new: Map<String, Structure>, val carriedFrom: Map<String, String>) One rebuild: the pointer (a ModulePointer, ComputeModulePointer, MeshModulePointer or RayTracingModulePointer), the structures old and new, and carriedFrom — for each new structure the generic carry filled, the name of the old structure it was filled from (itself, or the old name of a renamed block).
val hooks Called after the generic carry-over, once per rebuild; see the ordering above.
val accessed Called when Kotlin reaches a module's uniform structure by name (module.look), with the pointer, the name and the structure — how a stack.auto numbers block goes live when code or a keyframe target takes hold of it.
val onCreated Called when a module has just made its uniform structures, before anything has written them. A uniform buffer starts as whatever memory it was given, so a member that must read as zero until set — a stack.auto numbers block's foo_live — is written here.

RayTracingModule.kt

The ray-tracing counterpart of Module: a Vulkan ray-tracing pipeline built from raygen, miss, hit-group and callable shaders, with its shader binding table and bound resources.

RayTracingGroups lists the stages; RayTracingGroups.fromParameters builds it from the single-stage sources on Parameters. A RayTracingModulePointer lets a box swap a recompiled module in place, as ModulePointer does for rasterising modules. Work is recorded as RayTracingTasks: RayTracingDispatch traces a width × height × depth grid of rays, and rayTracingTask wraps an arbitrary command-buffer callback. rayTracingHostReadBarrier makes ray-tracing shader writes visible to a host read.

src/jvmMain/kotlin/afield/graphics/render/RayTracingModule.kt — read the source

Used in: Ray Tracing Examples

Top-level functions

Declaration Summary
fun RayTracingModule.pointer()
fun rayTracingTask(c: (inside: RayTracingModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer) -> Unit): RayTracingTask
fun rayTracingHostReadBarrier(cmd: CommandBuffer, buffer: VulkanBuffer? = null)

RayTracingShaderStage

data class RayTracingShaderStage(val name: String, val kind: ShaderKind, val source: String, val introspection: Introspection? = null, val sourceName: String = "$name.glsl", val includeResolver: ShaderIncludeResolver? = null)

One ray-tracing shader: its name, stage kind and GLSL source.

Declaration Summary
val name: String
val kind: ShaderKind
val source: String
val introspection: Introspection?
val sourceName: String
val includeResolver: ShaderIncludeResolver?

RayTracingHitGroup

data class RayTracingHitGroup(val name: String, val closestHit: RayTracingShaderStage? = null, val anyHit: RayTracingShaderStage? = null, val intersection: RayTracingShaderStage? = null)

Declaration Summary
val name: String
val closestHit: RayTracingShaderStage?
val anyHit: RayTracingShaderStage?
val intersection: RayTracingShaderStage?

RayTracingGroups

data class RayTracingGroups(var raygen: RayTracingShaderStage? = null, val miss: MutableList<RayTracingShaderStage> = mutableListOf(), val hit: MutableList<RayTracingHitGroup> = mutableListOf(), val callable: MutableList<RayTracingShaderStage> = mutableListOf())

Declaration Summary
var raygen: RayTracingShaderStage?
val miss: MutableList<RayTracingShaderStage>
val hit: MutableList<RayTracingHitGroup>
val callable: MutableList<RayTracingShaderStage>

RayTracingModulePointer

class RayTracingModulePointer(var current: RayTracingModule) : Renderable

Declaration Summary
var lastReloadReport: ModuleReloadReport? What the last successful reload did to the interface and which values it could not carry.
override fun render(command: CommandBuffer, bounds: Rectanglef)
fun reload(p: Parameters, forceReresolve: Boolean = true, onError: (String, Throwable?) -> Unit = p.errorHandler): Boolean
operator fun plusAssign(task: Pair<String, RayTracingTask>)+1 overload · also Named<RayTracingTask>
operator fun plusAssign(m: Pair<String, Target>)
fun enable()
fun disable()
override fun toString(): String

RayTracingFromUniform

class RayTracingFromUniform

Declaration Summary
operator fun getValue(inside: RayTracingModulePointer, property: KProperty<*>): Structure

RayTracingModule

class RayTracingModule(val parameters: Parameters) : Renderable

Declaration Summary
var valid
var enabled
val tasks
val before
val after
val groups: RayTracingGroups
val stageRecords
var uniforms: MutableMap<String, UpdatesDescriptorSet>
var liveUniformStructures: MutableMap<String, Structure>
var pipelineLayout: Long
var pipeline: Long
val pushConstantSize: Int
override fun render(command: CommandBuffer, bounds: Rectanglef)
fun enable()
fun disable()
fun copyTasksFrom(old: RayTracingModule)
fun textureUniform(name: String): Pair<HasImageViewArray, TextureSampler>?
fun cleanup() Release this module's pipeline, shader binding table, shader modules and descriptor layouts. It stops tracing at once; while its renderer is alive the handles go through the retirement queue, as for Module.cleanup — a full reload retires the module it replaces while frames that recorded it may still be in flight — and at once after the renderer has closed.
data class StageRecord(val stage: RayTracingShaderStage, val introspection: Introspection, val shaderModule: Long, val stageFlag: Int)
class AccelerationStructurePointer(val name: String, override val binding: Introspected.Bindable, val accelerationStructure: HasAccelerationStructure) : UpdatesDescriptorSet

RayTracingTask

interface RayTracingTask

Declaration Summary
fun trace(inside: RayTracingModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer)

RayTracingDispatch

class RayTracingDispatch(var width: Int, var height: Int = 1, var depth: Int = 1) : RayTracingTask

Declaration Summary
lateinit var inside: RayTracingModulePointer
var push
class BoundFromStructure<T>
class FromStructure<T>
override fun trace(inside: RayTracingModule, cmd: CommandBuffer, pushConstantBuffer: ByteBuffer)

ReloadCarryOver.kt

What survives when a module compiles a new shader and swaps it in: which uniform buffers keep their contents, which are reallocated, which are retired, and which of the values already written into them are re-applied.

Every module pointer finishes a reload through ReloadCarryOver.finishAndNotify, which runs this generic policy first and the ModuleRebuild hooks second, so a hook only fills in what the policy left. The policy reads the ShaderInterfaceTransition between the old and new introspections. A uniform block whose layout is unchanged keeps its buffer and its bytes, including across a rename, since the same members at the same offsets still mean what they meant. A block that was resized or retyped gets a fresh buffer, zeroed before anything writes it, and the values set on the old one are re-applied wherever they still fit the new reflection. A buffer the old module allocated and the new one does not use goes through the renderer's retirement queue rather than being freed under a frame that may still be reading it.

Only a buffer the module allocated for itself is zeroed, carried by layout or retired. One that came from the surrounding context, or that a resolver the box supplied handed back, is resolved again the way it was the first time and is never freed here. Zeroing applies to uniform blocks only: a storage buffer has its values carried and is retired the same way, but its contents are left as they are.

ModuleReloadReport is what the box side reads back: the transition, and one note per value that could not be carried, so a reload that silently dropped a uniform says which one.

src/jvmMain/kotlin/afield/graphics/render/ReloadCarryOver.kt — read the source

ModuleReloadReport

data class ModuleReloadReport(val transition: ShaderInterfaceTransition?, val notes: List<String> = emptyList())

What a module pointer's full reload did to the interface: the transition old→new (null when the pointer could not compare, for want of an introspection) and notes, one per value that could not be carried to the new module. Read by the box-side reload (reloadShader and siblings) to say it.

ReloadResolvers.kt

Keeps a module's resource resolvers one layer deep across repeated shader reloads.

Each module pointer installs its carry-over behaviour by replacing the resolvers on its Parameters. Module, ComputeModule and MeshModule replace all three of uboResolver, ssboResolver and textureResolver; RayTracingModule replaces the latter two. A reload is handed a copy of the parameters, and a copy brings the resolvers across as they are, so each wrap would otherwise land on the one the previous reload installed and every uniform, buffer and texture lookup would walk one closure more than it did on the run before.

reloadWrap unwraps before it wraps, so what sits underneath stays the resolver the box or moduleHere supplied and the depth stays at one however often a shader is reloaded. reloadWrapDepth reports that depth; ReloadResolversTest holds it at one across a hundred reloads, and checks that replacing the resolver yourself makes the replacement what the next reload wraps.

src/jvmMain/kotlin/afield/graphics/render/ReloadResolvers.kt — read the source

SplatCloudEdit.kt

Editable CPU-side Gaussian splat clouds: SplatCloud holds splats in world-unit scales, 0..1 opacity and linear colour, so cropping, filtering, decimating, transforming and merging are plain arithmetic.

A cloud converts from the table readCompressedSplatPly reads and writes back out as a .compressed.ply that CompressedSplatPly reads again.

src/jvmMain/kotlin/afield/graphics/render/SplatCloudEdit.kt — read the source

SplatCloud

class SplatCloud(val positions: FloatArray, val colors: FloatArray, val alphas: FloatArray, val rotations: FloatArray, val scales: FloatArray, val count: Int)

A Gaussian splat cloud as editable CPU data: read it, ask it questions, crop it, filter it, decimate it, move it, merge it, and write it back out as a .compressed.ply that CompressedSplatPly reads again.

Declaration Summary
val positions: FloatArray x, y, z per splat, world space.
val colors: FloatArray Linear r, g, b per splat in 0..1 — what the shader consumes, not the SH DC coefficient.
val alphas: FloatArray Opacity in 0..1, post-sigmoid.
val rotations: FloatArray Unit quaternion per splat, w, x, y, z — rot_0 first, as a 3DGS PLY spells it.
val scales: FloatArray sx, sy, sz per splat in world units, not logs.
val count: Int
data class CloudStats(val count: Int, val boundsMin: Vector3f, val boundsMax: Vector3f, val meanColor: Vector3f, val meanSaturation: Float, val meanAlpha: Float, val alphaPercentiles: FloatArray, val meanScale: Float, val scalePercentiles: FloatArray, val nonFinite: Int) What is actually in this cloud.
fun stats(): CloudStats
fun where(keep: (Int) -> Boolean): SplatCloud A new cloud holding only the splats keep accepts, in the same order.
fun cropped(min: Vector3f, max: Vector3f): SplatCloud Splats whose centres lie inside the axis-aligned box min..max, inclusive.
fun filtered(minAlpha: Float = 0f, maxAlpha: Float = 1f, minScale: Float = 0f, maxScale: Float = Float.POSITIVE_INFINITY): SplatCloud Splats within the given opacity and world-scale windows.
fun decimated(fraction: Float): SplatCloud A fraction of the splats, taken by a deterministic stride rather than at random.
fun transformed(transform: Matrix4f): SplatCloud Every splat through transform — position by the full affine map, rotation and scale by the linear part.
var shearLoss: Float How much shear transformed had to throw away: the largest absolute dot product it saw between two axes that should have been orthogonal, before it orthogonalised them. Zero after any transform the format can represent exactly.
fun withAlphaScaled(factor: Float): SplatCloud This cloud with every opacity multiplied by factor and clamped to 0..1.
fun withScale(factor: Float): SplatCloud This cloud with every world scale multiplied by factor.
fun toRawSplats(): CompressedSplatPly.RawSplats Re-quantize into CompressedSplatPly.RawSplats, so an edited cloud can go straight into GaussianSplatRenderer.packInto — and therefore into SplatRaster and into a live GPU Cloud.reupload — without a file in between.
fun write(file: File, mortonSort: Boolean = true) Write a valid PlayCanvas .compressed.ply. See CompressedSplatPlyWriter.

SplatCloudEditTest.kt

Tests the splat cloud edit loop: read a compressed PLY into a SplatCloud, query and edit it, preview it, and write it back with CompressedSplatPlyWriter.

It checks two claims that reading the code cannot settle:

  1. A cloud that goes out comes back. CompressedSplatPlyWriter is a new encoder and the only thing standing behind it is that it is the inverse of CompressedSplatPly's decoder. So: write, read back, and compare, with the error pinned at the digits the quantization actually delivers. And separately, write the same input through CompressedSplatPlyFixture — the independent transcription of splat-transform's own encoder that already lives here — and require the bytes to be identical, so the writer is checked against an encoder other than the reader it mirrors.
  2. The CPU transform is the transform the GPU bakes. SplatCloud.transformed decomposes and re-composes a rotation and three scales; packInto folds the same matrix into the covariance congruence and never writes it down. If those two disagree, an edited cloud looks different after it is saved, and no amount of float comparison inside either path would show it. So the test rasterizes both and compares pixels.

src/jvmMain/kotlin/afield/graphics/render/SplatCloudEditTest.kt — read the source

Top-level functions

Declaration Summary
fun main()

SplatCloudEditTest

object SplatCloudEditTest

The cases behind the four.splat-cloud-edit.smoke test; main runs them all.

Declaration Summary
fun runTests(): Boolean

SplatContactSheet.kt

src/jvmMain/kotlin/afield/graphics/render/SplatContactSheet.kt — read the source

SplatContactSheet

object SplatContactSheet

A whole splat sequence in one image: N frames rasterized small and tiled into a grid.

Declaration Summary
data class SheetCamera(val eye: Vector3f, val target: Vector3f, val up: Vector3f = Vector3f(0f, 1f, 0f), val fovY: Float = Math.toRadians(50.0).toFloat()) Where the camera goes. autoFrame is the useful default: an arbitrary capture's units and origin are not knowable in advance, so guessing an eye position produces an empty sheet and no hint as to why.
data class Layout(val tileWidth: Int = 192, val tileHeight: Int = 108, val columns: Int? = null, val gutter: Int = 3, val paper: Float = 0.06f, val labels: Boolean = true) How the grid is laid out.
class Sheet(val image: SplatRaster.SplatImage, val frames: List<Int>, val columns: Int, val rows: Int, val camera: SheetCamera, val tiles: List<SplatRaster.RasterStats>) A finished sheet: the image, the grid it used, and what went into each tile.
fun of(sequence: SplatSequence, frames: List<Int> = spread(sequence.frameCount, 24), layout: Layout = Layout(), camera: SheetCamera? = null, config: SplatRaster.RasterConfig = SplatRaster.RasterConfig(), packConfig: GaussianSplatRenderer.Config = GaussianSplatRenderer.Config(), transform: Matrix4f? = null): Sheet Rasterize frames of sequence into one tiled image.
fun spread(frameCount: Int, count: Int): List<Int> count frames spread evenly over frameCount, endpoints included.

SplatFusedPackTest.kt

Tests GaussianSplatRenderer.packInto — the fused compressed-file-to-GPU-layout path — against GaussianSplatRenderer.pack over the PlyFloatTable, which is the same computation the slow way.

The two are not bit-identical. The compressed format stores colour as linear 0..1 and opacity post-sigmoid, which is exactly what the shader consumes, so the fused path skips the f_dc = (c - 0.5) / SH_C0 and logit round trips that the table path performs and undoes. Every other quantity — position, scale, rotation, covariance — is computed by the same expressions in the same order and to the same spelling, so the tolerances below are tight enough that a swapped field or a wrong bit shift cannot pass.

src/jvmMain/kotlin/afield/graphics/render/SplatFusedPackTest.kt — read the source

Top-level functions

Declaration Summary
fun main()

SplatFusedPackTest

object SplatFusedPackTest

The cases behind the four.splat-fused-pack.smoke test; main runs them all.

Declaration Summary
fun runTests(): Boolean

SplatRaster.kt

A CPU rasterizer for Gaussian splat clouds, so a splat can be rendered to pixels, tested and captured without a GPU.

SplatRaster ports the vertex and fragment stages of GaussianSplatRenderer rather than defining a second look.

src/jvmMain/kotlin/afield/graphics/render/SplatRaster.kt — read the source

SplatRaster

object SplatRaster

A Gaussian splat cloud rasterized on the CPU, into pixels you can look at without a GPU.

Declaration Summary
const val SCREEN_DILATION The screen-space covariance dilation the shader adds to c00 and c11.
const val QUAD_SIGMAS How many σ the quad spans, matching vGaussianCoord = corner * 3.0.
data class View(val modelView: Matrix4f, val proj: Matrix4f, val width: Int, val height: Int) Everything the shader gets about where the camera is: the two push-constant matrices and the viewport size. Hand it the same matrices a GaussianSplatRenderer.Cloud would (camera.modelView() and camera.p.evaluateUniform()) and this rasterizer and that cloud frame the scene identically.
data class RasterConfig(val sizeScale: Float = 1f, val alphaScale: Float = 1f, val minScreenRadius: Float = 0.75f, val maxScreenRadius: Float = 128f, val alphaCutoff: Float = 1f / 255f, val colorGamma: Float = 2.2f, val compose: Compose = Compose.Over, val depthOrder: DepthOrder = DepthOrder.BackToFront) The rasterizer's knobs, named and defaulted to match GaussianSplatRenderer.Config and the vars on a GaussianSplatRenderer.Cloud, so a number that framed a cloud on the GPU frames it the same way here.
enum class DepthOrder In what order the splats are composited.
enum class Compose The CPU equivalents of GaussianSplatRenderer.Blend's five named companions, all on premultiplied source (c·a, a) as the fragment shader emits it.
class SplatImage(val width: Int, val height: Int) A rasterized image: premultiplied linear RGBA in floats, four per pixel, row 0 at the top.
data class ImageStats(val width: Int, val height: Int, val meanLuminance: Float, val peakLuminance: Float, val coverage: Float, val distinctColors: Int) See SplatImage.stats.
data class RasterStats(val considered: Int, val drawn: Int, val culledBehind: Int, val culledTransparent: Int, val culledOffscreen: Int, val nonFinite: Int, val fragments: Long) What the rasterizer did, per splat. Every splat is in exactly one of the counted categories, and check enforces that: the five counts have to add up to the number considered, so a category nothing counts fails the total rather than passing unnoticed.
data class Rasterized(val image: SplatImage, val stats: RasterStats) An image and the account of how it was made.
fun rasterize(packed: FloatBuffer, count: Int, view: View, config: RasterConfig = RasterConfig(), into: SplatImage = SplatImage(view.width, view.height)): Rasterized Rasterize count splats held in packed in GaussianSplatRenderer's 16-float GPU layout — the buffer GaussianSplatRenderer.packInto fills.
fun rasterize(sources: List<GaussianSplatRenderer.SplatSource>, view: View, config: RasterConfig = RasterConfig(), packConfig: GaussianSplatRenderer.Config = GaussianSplatRenderer.Config(), sortAlong: Vector3f? = null, into: SplatImage = SplatImage(view.width, view.height)): Rasterized Rasterize compressed splat payloads directly: GaussianSplatRenderer.packInto into a scratch buffer, then rasterize over it. This is the whole pipeline the GPU runs, minus the GPU.
fun allocatePacked(splats: Int): FloatBuffer A direct FloatBuffer sized for splats in the 16-float layout.
fun linearToSrgb(value: Float): Float The sRGB OETF, which is what a _SRGB framebuffer applies on write.
fun srgbToLinear(value: Float): Float The sRGB EOTF, the inverse of linearToSrgb.

SplatRasterTest.kt

Tests SplatRaster at the level of pixels.

SplatFusedPackTest checks that the two pack paths produce the same floats, and every assertion in it would still hold if the shader drew the cloud mirrored, upside down, or with red and blue swapped. This is the test that cannot. It asserts:

  • Orientation, from four corner-marked splats — the only proof of the Y and X conventions that does not reduce to re-deriving the same viewport call the rasterizer was written from.
  • A known splat at a known pixel in a known colour, with the pixel derived on paper from the perspective matrix and written here as a literal, so a change in the projection has to be noticed rather than absorbed.
  • Slot sensitivity — the differential oracle. Each of the sixteen floats in GaussianSplatRenderer's layout is perturbed in turn, and the image is required to move for the thirteen the shader reads and to not move for the three it does not, which tests that the CPU and the GPU read the same cloud.
  • Pixel agreement between the two pack paths, which is SplatFusedPackTest's float claim carried through to what a person actually sees.
  • That the poison detector detects poison. A cloud is broken with NaNs and the non-finite count is required to find exactly them — and required to be zero on good data, so the detector cannot be passing by always firing.

src/jvmMain/kotlin/afield/graphics/render/SplatRasterTest.kt — read the source

Top-level functions

Declaration Summary
fun main()

SplatRasterTest

object SplatRasterTest

The cases behind the four.splat-raster.smoke test; main runs them all.

Declaration Summary
fun runTests(): Boolean

SplatTools.kt

A command line over the CPU splat toolchain, for looking at a capture without opening Field.

run-jvm-main afield.graphics.render.SplatToolsKt sheet ~/Downloads/circle-05-a120p0_00001_ out/
run-jvm-main afield.graphics.render.SplatToolsKt frame ~/Downloads/shot/frame_000060.compressed.ply out/

sheet draws a contact sheet of frames spread across a capture directory; frame rasterizes one compressed PLY. Both run on the CPU through SplatRaster, so judging a fresh capture (is it any good, which frames are worth looking at) needs no GPU, window or scrubbing.

src/jvmMain/kotlin/afield/graphics/render/SplatTools.kt — read the source

Top-level functions

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

SplatTools

object SplatTools

The sheet and frame commands behind this file's main.

Declaration Summary
data class Written(val file: File, val bytes: Int, val note: String) What sheet and frame wrote, so a caller can report it without re-reading the files.
fun sheet(directory: File, outputDir: File, tiles: Int = 24, layout: SplatContactSheet.Layout = SplatContactSheet.Layout(), config: SplatRaster.RasterConfig = SplatRaster.RasterConfig(sizeScale = 2.5f, alphaScale = 1.6f)): Written A contact sheet of directory's frames into outputDir.
fun describe(path: File): String Print what is in a cloud, without drawing it: SplatCloud.CloudStats over one file.
fun frame(path: File, outputDir: File, width: Int = 800, height: Int = 450, config: SplatRaster.RasterConfig = SplatRaster.RasterConfig(sizeScale = 2.5f, alphaScale = 1.6f)): Written One frame of a sequence, or one standalone .compressed.ply, at width x height.

StandardDrawing.kt

The small vocabulary of FLine construction and styling that sketches and examples reach for first.

fline is the builder; the rangeTo operator makes a one-segment line out of two vectors (a..b, composable with +=), and the styling overloads of moveTo / lineTo / cubicTo position a node and style it in the same call. Appearance is carried by delegated properties on FLine and FLine.Node — color, thickness, thicken, filled, stroked, pointed, pointSize — which the renderers read back; onClick and hoverColor only do anything in the interactive outputs.

The rest are shape helpers: rect and Rect.toFLine, arrow (a real filled arrowhead rather than a point sprite), seg, and fitInto, which scales and centres a list of lines into a Rect using their 2D bounds only.

src/jvmMain/kotlin/afield/graphics/render/StandardDrawing.kt — read the source

Used in: FLine examples (Field graphics), Points into structure — Voronoi, Apollonius, contours and clusters, Three.js scenes with three { … }

Top-level functions

Declaration Summary
fun rect(x: Number, y: Number, w: Number, h: Number) A plain Rect from x, y, w, h — the frame type fitInto, Rect.toFLine and the lines(...) outputs all speak.
inline fun fline(block: FLine.() -> Unit): FLine Preferred builder for FLine geometry in sketches and examples.
operator fun Vec3.rangeTo(other: Vec3): FLine A single straight segment written as a range: vec(0, 0)..vec(1, 1). Composes with += for chaining, e.g. f += vec(2, 0)..vec(3, 3).
operator fun Vec2.rangeTo(other: Vec2): FLine 2D overload of the segment range operator.
inline fun FLine.moveTo(v: Vec3, style: FLine.Node.() -> Unit): FLine Add a moveTo and immediately style the resulting node: moveTo(v) { color = RED }.
inline fun FLine.lineTo(v: Vec3, style: FLine.Node.() -> Unit): FLine Add a lineTo and immediately style the resulting node.
inline fun FLine.cubicTo(c1: Vec3, c2: Vec3, to: Vec3, style: FLine.Node.() -> Unit): FLine Add a cubic and immediately style the resulting node.
fun seg(a: Vec2, b: Vec2, pointed: Boolean = false, color: Vec4 = vec(1, 1, 1, 1)) One straight 2D segment from a to b, styled in the same call — the named form of fline { moveTo(a); lineTo(b) }, and what the graph and network examples draw their edges with.
fun FLine.rect(x: Number, y: Number, w: Number, h: Number): FLine Append a closed rectangle path to this line.
fun FLine.rect(rect: Rect): FLine Append a closed rectangle path for rect to this line.
fun FLine.grid(bounds: Rect, nx: Int, ny: Int): FLine Append a rectangular grid of nx by ny cells, retaining this line's existing paths and style. Each grid line starts a separate path: nx + 1 vertical and ny + 1 horizontal segments.
fun FLine.circle(cx: Number, cy: Number, r: Number, segments: Int = 0): FLine Append a closed circle of radius r about (cx, cy) to this line, starting and ending at its rightmost point.
fun Rect.toFLine(color: Vec4 = vec(1.0, 1.0, 1.0, 1.0), filled: Boolean = false, stroked: Boolean = true, thickness: Double? = if (stroked) 1.0 else null): FLine Convert this rectangle into a styled FLine.
fun List<FLine>.fitInto(frame: Rect, padding: Double = 0.0, preserveAspect: Boolean = true): List<FLine> Scale and translate these lines so their 2D bounds fit inside frame.
fun arrow(x0: Double, y0: Double, x1: Double, y1: Double, color: Vec4 = vec(1.0, 1.0, 1.0, 1.0), thickness: Double = 2.0, headLength: Double = Double.NaN, headWidth: Double = Double.NaN, startHead: Boolean = false, endHead: Boolean = true): FLine Build an arrow with a real arrowhead instead of relying on point sprites.
fun arrow(from: Vec2, to: Vec2, color: Vec4 = vec(1.0, 1.0, 1.0, 1.0), thickness: Double = 2.0, headLength: Double = Double.NaN, headWidth: Double = Double.NaN, startHead: Boolean = false, endHead: Boolean = true): FLine Build an arrow with configurable arrowheads.

Top-level values

Declaration Summary
var FLine.color: Vector4f The line's colour as RGBA in 0..1 (sRGB, alpha included) — one colour for the stroke, the fill and the node dots alike. Opaque white when never set.
var FLine.Node.color: Vector4f Per-node colour override in 0..1, which is how one FLine carries a gradient: the GPU point/triangle writers in afield.graphics.render.StandardDrawing use a node's own colour where it has one and the whole line's color where it does not.
var FLine.thickness: Double? Stroke width, in the same units as the line's own points (pixels, for the 2D outputs). null — the default — means "unset", which every renderer reads as 1.0 rather than as "invisible".
var FLine.thicken: Stroke? An AWT Stroke that replaces "stroke at one width" with "convert this line into the outline of its own stroke" — dashes, tapers, calligraphic pens, anything Stroke.createStrokedShape can do.
var FLine.filled: Boolean Whether closed subpaths are flooded with color. false by default, and independent of stroked — with both true the renderers fill and then draw the outline on top.
var FLine.stroked: Boolean Whether the path is stroked at thickness. true by default; with this and filled both false the geometry still exists and still hit-tests, but nothing is drawn except the node dots that pointed asks for.
var FLine.pointed: Boolean Draw a dot at every node of the line — every moveTo/lineTo/cubicTo endpoint, not the interior control points of a cubic and not close. Off by default. The fast way to see where a path's points actually landed, and how a particle set gets drawn at all.
var FLine.pointSize: Double Size of the dots pointed draws, 1.0 by default, overridable node by node with FLine.Node.pointSize.
var FLine.Node.pointSize: Double? Per-node override of the line's FLine.pointSize, under the same * 3 radius convention. null — the default, and unlike the line-level property this one is not written back on read — means "follow the line".
var FLine.onClick: ((FLineIntersection) - Callback invoked when this FLine is clicked in an interactive output.
var FLine.label: String? A human name for this line, reported back as FLineIntersection.label when it is clicked and used as the heading by afield.ui.visualization.inspect.
var FLine.hoverColor: Vec4? Color to show when the pointer hovers over this FLine in an interactive SVG output. When null no hover color change is applied.

FLineHitKind

enum class FLineHitKind

What kind of thing on a figure a click landed on — the same three-way distinction the editable FLine widget already draws its hit targets with (data-fline-hit-kind in sliders.js), now reported to FLine.onClick as well.

Constants: POINT, EDGE, FACE, UNKNOWN.

FLineFaceCoordinates

enum class FLineFaceCoordinates

Which convention FLineIntersection.u and v follow for a FLineHitKind.FACE hit — because there is no one right answer for a face nobody chose, and a plausible wrong number is worse than a labelled one.

Constants: NONE, BILINEAR, BOUNDING_BOX.

FLineIntersection

data class FLineIntersection(val closestVertexIndex: Int, val position: Vec3, val kind: FLineHitKind = FLineHitKind.UNKNOWN, val lineIndex: Int = -1, val label: String? = null, val nodeIndex: Int = -1, val subPathIndex: Int = -1, val elementIndex: Int = -1, val u: Double = Double.NaN, val v: Double = Double.NaN, val faceCoordinates: FLineFaceCoordinates = FLineFaceCoordinates.NONE, val onElement: Vec3? = null)

Hit information passed to FLine.onClick from the interactive line renderers — what a click on a figure returns.

Declaration Summary
val closestVertexIndex: Int Index into the line's nodes of the node nearest the click, or -1 when the client did not work one out.
val position: Vec3 Where the click landed, in the line's own coordinate space. The three.js output fills in all three components; the 2D SVG output leaves z at 0 — onElement is the one to read for a depth, because it interpolates z along the element that was hit.
val kind: FLineHitKind Which kind of element was hit. FLineHitKind.UNKNOWN when the output does not distinguish.
val lineIndex: Int Index of the clicked line within the lines passed to that one lines(...) call, or -1 when unknown. The point of it: one shared onClick closure can serve a whole figure, instead of one closure per part.
val label: String? The clicked line's FLine.label, if it had one.
val nodeIndex: Int Index into the line's nodes of the node that is this element: the node itself for a FLineHitKind.POINT, the node that draws the segment for an FLineHitKind.EDGE, the Close for a FLineHitKind.FACE. -1 when unknown. Unlike closestVertexIndex this is the element, not the nearest dot.
val subPathIndex: Int Which subpath (moveTo-delimited piece) of the line the element belongs to, or -1.
val elementIndex: Int Ordinal of the element among its own kind within the line — edge 0, 1, 2…; face 0, 1… Or -1.
val u: Double Where on the element the click landed, 0..1, or NaN when it does not apply.
val v: Double The second face coordinate for an FLineHitKind.FACE, NaN otherwise. See faceCoordinates.
val faceCoordinates: FLineFaceCoordinates Which convention u and v follow for a face hit — bilinear on a four-corner face, its bounding box otherwise, or a refusal. Always FLineFaceCoordinates.NONE for a non-face hit.
val onElement: Vec3? The point on the element nearest the click, in the line's own coordinate space, or null.

groups.kt

Mesh groups: lists of meshes that share one live push-constant binding.

MeshGroup, MeshMeshGroup and ComputeMeshGroup each carry a BoundFromStructure delegate, so a property assigned on the group (a supplier () -> T) is bound to the push constant of the same name in every member, and read back from the first member that has it. A name no member's module declares as a push constant throws. bindPush is the looser form for meshes that are not in a group: it runs one binding block against each target in turn.

src/jvmMain/kotlin/afield/graphics/render/groups.kt — read the source

Top-level functions

Declaration Summary
fun <T> bindPush(vararg targets: T, bindings: T.() -> Unit) Apply the same live push bindings to each mesh: bindPush(even, odd) { count = { n } }. The receiver retains its concrete mesh type and generated shader properties. The block executes once per target; suppliers assigned inside it remain live rather than being sampled here.

MeshGroup

class MeshGroup : ArrayList<Mesh>()

A list of Meshes whose push constants are bound together by property name.

Declaration Summary
class BoundFromStructure<T>

MeshMeshGroup

class MeshMeshGroup : ArrayList<MeshMesh>()

Declaration Summary
class BoundFromStructure<T>

ComputeMeshGroup

class ComputeMeshGroup : ArrayList<ComputeMesh>()

Declaration Summary
class BoundFromStructure<T>

DirectLighting.kt

Direct lighting from one-sided rectangular area emitters, shaded over a raster G-buffer.

DirectLighting is a render target that DirectLighting.attach adds to a render list's pre-targets. It reads DirectLightSurfaces: world position and depth, normal, albedo, the unlit source image and motion. It lights those surfaces from an AreaEmitter, tracing shadow rays against an OpaqueRayScene when the device supports ray queries; otherwise its mode defaults to passing the source image through, with an unshadowed mode available. DirectLighting.enableRtxdi adds optional RTXDI reservoir resampling, configured by RtxdiSettings. The view setting switches the output to debug images such as normals, depth or light id.

src/jvmMain/kotlin/afield/graphics/render/lighting/DirectLighting.kt — read the source

Used in: Direct lighting

AreaEmitter

data class AreaEmitter(val center: Vector3f, val edgeU: Vector3f, val edgeV: Vector3f, val radiance: Vector3f)

One-sided rectangle: center plus full edge vectors. cross(edgeU,edgeV) faces the emitting side. Radiance is unclamped, linear RGB. Values are snapshotted by DirectLighting.render.

Declaration Summary
fun validate()

DirectLightSurfaces

data class DirectLightSurfaces(val position: HasImageView, val normal: HasImageView, val albedo: HasImageView, val source: HasImageView, val motion: HasImageView)

Raster surface contract. XYZ world position + Vulkan depth [0,1] (clear=1, not reversed); XYZ world shading normal; linear RGB albedo. Motion is currentUV-previousUV, Y down, unjittered. Point sampling only. Material identity, emission, eligibility and temporal validity are NOT alpha. The optional RTXDI bridge snapshots these inputs; material IDs are not encoded in alpha.

Declaration Summary
val albedo: HasImageView, val source: HasImageView, val motion: HasImageView)

DirectLighting

class DirectLighting(val renderer: Renderer, val surfaces: DirectLightSurfaces, val visibility: OpaqueRayScene, val width: Int, val height: Int, val name: String = "direct-light") : Target, AutoCloseable

Experimental, mono, raster-primary direct light, with optional pinned RTXDI resampling. Insert after the producer of surfaces on the graphics queue. All mutations, reload and close occur on Field's render thread between frames.

Declaration Summary
val width: Int, val height: Int, val name: String
enum class Mode
enum class View
var mode
var view
var samples
var seed
var epsilon
var effectWeight
var emitter
val output: RenderToTexture
fun reload(source: String): Result<Unit> Compile first, reject changed layouts, then swap. Any failure preserves the working pipeline.
fun enableRtxdi(program: RtxdiProgram, settings: RtxdiSettings): Result<Unit> Optional SDK activation is transactional; a failed compile preserves the current pass.
fun clearRtxdiTimings()
fun reloadRtxdi(program: RtxdiProgram): Result<Unit>
override fun render(command: CommandBuffer,index: Int)
fun attach(target: afield.graphics.vulkan.targets.RenderTarget = renderer.renderList): DirectLighting
fun readLinear(): FloatArray Blocking raw FP32 readback: RGB is linear lighting (or diagnostic), W is visibility (-1 for background), not coverage. SOURCE preserves input alpha.
fun readPresentedLinear(): FloatArray The linear RGB presented by output, including optional reconstruction. Raw readback is unchanged.
override fun close()

DirectLightingDemo.kt

A small scene for trying DirectLighting: a floor, three cubes and a moving area light.

DirectLightingDemo builds the raster renderer, the OpaqueRayScene and the lighting pass at a given size; call DirectLightingDemo.animate from the box's frame loop and DirectLightingDemo.moveObject to move a cube. Re-running the box that built it closes the previous instance. DirectLightingSmoke is the standalone entrypoint behind the four.direct-lighting.smoke smoke test, with a --fallback mode that runs without ray tracing.

src/jvmMain/kotlin/afield/graphics/render/lighting/DirectLightingDemo.kt — read the source

DirectLightingDemo

class DirectLightingDemo(val renderer: Renderer, width: Int=640,height: Int=360) : AutoCloseable

A reusable live-coding scene. Camera is bundle.scene.camera; call animate from the box frame loop.

Declaration Summary
val bundle
val visibility
val cube
val floor
val instances
val meshes
val lighting: DirectLighting
fun animate(seconds: Double)
fun moveObject(index: Int, transform: Matrix4f)
override fun close()

DirectLightingSmoke

object DirectLightingSmoke

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

DirectLightingQualitySmoke.kt

src/jvmMain/kotlin/afield/graphics/render/lighting/DirectLightingQualitySmoke.kt — read the source

DirectLightingQualitySmoke

object DirectLightingQualitySmoke

Full-resolution, linear-light quality checks and actual GPU documentation captures. Separate from the small estimator acceptance suite so its statistical tests stay inexpensive.

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

LightingGpuTimer.kt

How long a lighting pass took on the GPU, timed per swapchain image.

Two timestamps per image, top of pipe to bottom, reused once the renderer has waited on that image's fence. Each begin collects the previous result if it is available, so reading one never costs a CPU wait, and the last 1024 readings stay in samples in milliseconds for a live box.

The queries are a slice of a pool shared with other timers (TimestampSlabs), not a pool of their own: KosmicKrisp backs every VkQueryPool with a Metal counter heap, and a process gets 32 of those, each at most 4096 timestamps (measured, SDK 1.4.363, M1 Max), which a pool per timer exhausts. A timer that cannot get a slice records nothing.

src/jvmMain/kotlin/afield/graphics/render/lighting/LightingGpuTimer.kt — read the source

OpaqueRayScene.kt

Opaque triangle geometry for ray-query visibility, as used by DirectLighting for shadows.

OpaqueTriangles is an immutable indexed mesh (OpaqueTriangles.box makes a box), and an OpaqueInstance places one with a stable id and transform. OpaqueRayScene.commit takes the instance list and builds one bottom-level acceleration structure per geometry and a new top-level structure for each edit, waiting for the build, so commit between frames on the render thread. OpaqueRayScene.available reports whether the device supports ray queries.

src/jvmMain/kotlin/afield/graphics/render/lighting/OpaqueRayScene.kt — read the source

Used in: Direct lighting

OpaqueTriangles

class OpaqueTriangles(positions: FloatArray, indices: IntArray)

Immutable indexed XYZ triangles. Opaque, double sided visibility only; no alpha or transmission.

OpaqueInstance

data class OpaqueInstance(val id: Int, val geometry: OpaqueTriangles, val transform: Matrix4f = Matrix4f())

Stable object id is independent of list position. Transform is copied at OpaqueRayScene.commit.

OpaqueRayScene

class OpaqueRayScene(val renderer: Renderer) : HasAccelerationStructure, AutoCloseable

Static BLAS per geometry identity; a new TLAS for each explicit instance edit. No refit is claimed. Commit on the render thread between frames. Builds submit on Field's graphics queue and wait for their fence; removed resources retire via Field's frames-in-flight queue. Rebuilding rather than refitting makes edits safe, at the cost of a stall on instance or topology edits.

Declaration Summary
var blasBuilds
var tlasBuilds
var closed
override fun deviceAddress(): Long
fun commit(instances: List<OpaqueInstance>): Boolean Returns false when unchanged or unavailable. Empty scenes are represented by a masked instance.
override fun close()

RtxdiEngine.kt

The RTXDI implementation behind DirectLighting's two RTXDI modes: reservoir resampling run entirely as compute passes over GPU buffers, with none of the SDK's CPU-side wrapper linked in.

An engine is built when RTXDI is enabled and sized from the render dimensions it finds then, and it owns what that sizing implies: a packed surface buffer, host-visible buffers per swapchain image for the camera matrices and for this frame's and the previous frame's area lights, and reservoir storage of three slices of 24 bytes a pixel, rounded up to whole 16×16 tiles. That allocation is checked against the device's maxStorageBufferRange as the engine is built, so an oversized render resolution is refused there rather than inside a descriptor write. Ray queries are required.

Reuse across frames holds only while the history does. Every frame compares the projection, the camera (a view difference over 0.75 counts as a cut), the acceleration structure's build count and a signature of the sampling settings, and any change resets the history and records which of them it was. A reference render runs a single reference estimator in place of the four resampling stages and leaves no history behind. The reconstruction filter that can follow runs over the radiance view alone, and feeds neither the estimator nor the reservoirs.

src/jvmMain/kotlin/afield/graphics/render/lighting/RtxdiEngine.kt — read the source

RtxdiLightingSmoke.kt

src/jvmMain/kotlin/afield/graphics/render/lighting/RtxdiLightingSmoke.kt — read the source

RtxdiLightingSmoke

object RtxdiLightingSmoke

Optional GPU acceptance check: build the pinned shaders before run-jvm-main.

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

RtxdiProgram.kt

Loads the optional RTXDI shaders, and refuses a binary whose interface disagrees with the host code written against it.

Field does not ship NVIDIA's sampling SDK and loads nothing from it at startup. Running tools/lighting/build_rtxdi.py --prepare clones the SDK and fetches a shader compiler into ignored build storage, and the build leaves a manifest under build/lighting-experiment/rtxdi-shaders/. load reads that manifest; with no manifest there it fails with the command to run.

What load then checks is the interface rather than the file. The manifest has to record the SDK commit this source was written against, the SPIR-V has to hash to what the manifest recorded, and the shader's own reflection has to still carry the workgroup size, the push-constant block's size and member offsets, the reservoir and offset array strides, and the surface and light struct layouts that the host side writes into. Shaders rebuilt from a different SDK revision therefore fail to load, instead of being handed the host's bytes at offsets that have moved.

A loaded program is passed to DirectLighting through enableRtxdi, which builds the pass and leaves the current one in place if that fails.

src/jvmMain/kotlin/afield/graphics/render/lighting/RtxdiProgram.kt — read the source

RtxdiProgram

class RtxdiProgram private constructor(val manifest: JSONObject, internal val reflection: Introspection)

Optional compiled SDK shaders. Build with tools/lighting/build_rtxdi.py; no SDK is loaded at startup.

RtxdiSettings.kt

Live controls for the RTXDI resampling path of DirectLighting.

RtxdiSettings holds the camera matrices, temporal and spatial reuse parameters, shadowing and an optional spatial denoise, and takes the light list as IdentifiedAreaLights through RtxdiSettings.commitLights, so a light keeps its history across reordering. Edit it on the render thread between frames; RtxdiSettings.resetHistory discards history after a cut or material edit.

src/jvmMain/kotlin/afield/graphics/render/lighting/RtxdiSettings.kt — read the source

Used in: Direct lighting

IdentifiedAreaLight

data class IdentifiedAreaLight(val id: Int, val emitter: AreaEmitter)

Stable light identity, independent of order. Opaque one-sided area emitters only.

RtxdiSettings

class RtxdiSettings(val view: () -> Matrix4f, val projection: () -> Matrix4f)

Live RTXDI controls. Commit lights and edit these values between frames on Field's render thread. Current/previous camera matrices use Field's Vulkan projection (the raster viewport flips Y). Geometry commits invalidate all history; material semantics edits must call resetHistory.

Declaration Summary
val view: () -> Matrix4f The camera's view matrix, read once per frame rather than stored, so a camera that moves between frames needs no further bookkeeping here.
val projection: () -> Matrix4f The camera's projection matrix, read per frame like view. Field's Vulkan convention, where the raster viewport flips Y.
var denoisePasses Surface-guided spatial reconstruction, separate from reservoir reuse. Zero preserves raw output. No color history is stored, so moving lights and camera cuts cannot leave color trails. Each pass doubles the pixel spacing; three passes have a 14-pixel support radius.
var temporal Whether each pixel may reuse the reservoir its surface carried last frame. Off, every frame resamples from scratch: noisier, and the only setting under which a still camera does not keep improving.
var spatialSamples How many neighbouring pixels' reservoirs each pixel resamples, 0 to 32. This is the reuse that cleans up a single frame, and the cost that scales with it.
var maxHistory The largest number of frames' worth of samples a reservoir may claim to hold, 1 to 32. Capping it is what lets a reservoir forget light that has moved; raising it trades responsiveness for a quieter image.
var spatialRadius The radius in pixels that spatial neighbours are drawn from, 0 to 64.
var normalThreshold How closely two surface normals must agree before one's reservoir may be reused for the other, as a dot product from 0 to 1 — the default admits normals within about 18° of each other, which is what keeps light from bleeding around a crease. Handed to both the temporal and the spatial resampling pass.
var depthThreshold The companion depth test, from 0 to 0.2, so that a pixel sitting on a surface behind this one is not reused for it. Handed to the same two passes as normalThreshold.
var shadows Whether a shadow ray is traced towards the chosen light. Off, every sample is treated as visible, which is faster and wrong wherever anything occludes anything.
var animateSeed Whether the sampling seed advances with the frame counter. Off, every frame draws the same sequence, which is what makes two renders of one scene comparable pixel for pixel.
var resetReason The reason last passed to resetHistory, or initial before any call. The renderer reads it when it notices the reset and reports it as its own reason for that frame, which is how a deliberate cut can be told apart from the resets it detects for itself — a geometry commit, a change of projection, a large camera move, or an edit to temporal, spatialSamples, spatialRadius, maxHistory or shadows, which are the settings that invalidate history.
fun resetHistory(reason: String = "explicit cut or edit")
fun commitLights(values: List<IdentifiedAreaLight>) Transactional snapshot. Moving/reordered lights retain their IDs; deleted IDs map to -1.

megaTextures.kt

A directory of images as one texture, however many of them there are.

Box.megaTextureHere is the whole box-facing surface: point it at a folder and it scans the folder, builds the atlas, and registers the GLSL your shaders interpolate with {{ mega.all }}. The machinery behind it lives in afield.graphics.texture.mega, and the reason it is worth reaching for rather than a pile of LoadedTextures is that it is GPU-driven: the fragment shader measures its own on-screen footprint and asks for the resolution it needs, so nothing here has to be told where anything is or how big it looks.

dev_docs/agents/transcripts/mega_texture_wall.yaml is the worked example — five lines of Kotlin and two shaders you can edit live.

src/jvmMain/kotlin/afield/graphics/render/megaTextures.kt — read the source

Top-level functions

Declaration Summary
fun Box.megaTextureHere(build: MegaTextureSpec.() -> Unit): MegaTexture Build (or reuse) this box's MegaTexture and register its GLSL under {{ <templateName>.… }}.
fun Box.megaTexture(templateName: String = "mega"): MegaTexture? This box's MegaTexture under templateName, if megaTextureHere has built one.

MegaTextureSpec

class MegaTextureSpec

What to show and how, for Box.megaTextureHere.

Declaration Summary
val folders Directories to take images from, in the order given.
var recursive: Boolean Walk subdirectories too.
var extensions: Set<String> Lower-case file extensions to take, without the dot. Anything else in the folder is ignored.
var maxImages: Int Stop after this many images — useful for trying a huge directory at a small size first.
var templateName: String The shaderTemplate key the GLSL slots are registered under: {{ mega.all }} by default.
var warmBaseLevel: Boolean Queue level-0 pages for everything up front, so the wall fills in without being looked at.
val config Page sizes, residency budget, decode threads, bindings — see MegaTextureConfig.
fun folder(path: String) Add a directory of images, by path.
fun folder(path: Path): MegaTextureSpec Add a directory of images.

OpenDlssNrGpuSmoke.kt

src/jvmMain/kotlin/afield/graphics/render/neural/OpenDlssNrGpuSmoke.kt — read the source

OpenDlssNrGpuSmoke

object OpenDlssNrGpuSmoke

Managed-runner entry point: real Field device, native pass, caller-owned submission and head readback. Arguments: image, output-directory, tone, structure, style. Diagnostics use CPU readback; the normal pass does not. Pair the resulting head with the standalone render for parity.

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

OpenDlssNrImage.kt

Runs one image file through OpenDlssNrPass and shows the result on screen.

OpenDlssNrImage loads the image and requires OpenDlssNrSupport.availability to be ready. After OpenDlssNrImage.install it evaluates the pass each frame under the renderer, with controls for the network and exposure, gamma and offset for grading the result as it reaches the screen.

src/jvmMain/kotlin/afield/graphics/render/neural/OpenDlssNrImage.kt — read the source

OpenDlssNrImage

class OpenDlssNrImage(val renderer: Renderer, val path: String, val name: String = "opendlssnr-image")

Editable image study. Install below a renderer; inference precedes the grading/display module. close detaches immediately and retires allocations after submitted frames finish.

Declaration Summary
val pass
val output: RenderToTexture
var controls: OpenDlssNrControls
var exposure Linear RGB multipliers, independent of the network condition.
var gamma
var offset
fun install(): OpenDlssNrImage
override fun render(command: CommandBuffer, index: Int)
override fun close()

OpenDlssNrPass.kt

The Vulkan pass that runs the native neural-rendering (NR) network over a sampled image.

OpenDlssNrPass.evaluate takes OpenDlssNrInputs (colour with its OpenDlssNrColorEncoding, and optional motion and depth guides for temporal mode), an OpenDlssNrFrame and the current OpenDlssNrControls (seed, style, tone, structure, intensity), and writes the result to its output texture. OpenDlssNrPass.postEffect wraps it as a post effect, and OpenDlssNrPass.reset drops temporal history after a cut or edit.

src/jvmMain/kotlin/afield/graphics/render/neural/OpenDlssNrPass.kt — read the source

Used in: Neural image rendering with OpenDLSS-NR

OpenDlssNrColorEncoding

enum class OpenDlssNrColorEncoding

What a Vulkan sampler returns. SRGB-format views already decode, so use LINEAR_LDR for those.

Constants: SRGB_CODE, LINEAR_LDR, LINEAR_HDR.

OpenDlssNrControls

data class OpenDlssNrControls(val seed: Int=1,val style: Int=1,val tone: Float=1f,val structure: Float=1f, val skin: Float=-1f,val autoMask: Boolean=false,val intensity: Float=1f,val temporal: Boolean=false)

Style is the network condition, independently of grading after the pass. Tone and structure take any finite binary16 value: -10..30 is useful for image exploration. Skin is active only with autoMask; negative skin inherits structure. Fixed seed is the default.

Declaration Summary
val skin: Float

OpenDlssNrInputs

data class OpenDlssNrInputs(val color: HasImageView,val encoding: OpenDlssNrColorEncoding, val motion: HasImageView?=null,val depth: HasImageView?=null,val paperWhite: Float=1f, val motionCurrentMinusPrevious: Boolean=true,val preserveAlpha: Boolean=true,val motionInPixels: Boolean=false)

Top-left origin, same-sized sampled images. Motion is UV displacement, by default Field's current-minus-previous convention; depth is standard Vulkan depth with background 1. Temporal mode needs both guides. An arbitrary image/video must supply real motion to use history.

Declaration Summary
val motion: HasImageView?
val motionCurrentMinusPrevious: Boolean

OpenDlssNrFrame

data class OpenDlssNrFrame(val index: Long,val revision: Long=0,val reset: Boolean=false)

OpenDlssNrPass

class OpenDlssNrPass(val renderer: Renderer,val width: Int,val height: Int) : AutoCloseable

Optional native Vulkan pass, recorded outside render passes on Field's graphics queue. Construct/evaluate/edit/close on the renderer thread. Recreate at a new resolution; close retires behind Field's completion fences. The output is linear RGBA16F, ready for normal Field grading. No CPU frame transfer or private per-frame submit occurs. Failure is explicit; callers can use availability() before creating the pass and present the source when unavailable.

Declaration Summary
val output: RenderToTexture
val nativeAllocatedBytes: Long Native VkDeviceMemory allocations, including the shared model/cache and upload staging; excludes driver-internal pipeline/PTX allocations and other passes' history images.
val outputAllocatedBytes: Long
var controls
val isClosed: Boolean Whether this pass has retired, explicitly or with its renderer.
var resetCount
var resetReason
fun reset(reason: String="camera cut or live edit")
fun postEffect(name: String,inputs: () -> OpenDlssNrInputs,frame: () -> OpenDlssNrFrame): FabricatePostEffect Configure an editable Fabricate post effect; its native target calls the same evaluate path.
fun evaluate(command: CommandBuffer,inputs: OpenDlssNrInputs,frame: OpenDlssNrFrame)
override fun close()

OpenDlssNrSmokeTest.kt

src/jvmMain/kotlin/afield/graphics/render/neural/OpenDlssNrSmokeTest.kt — read the source

OpenDlssNrSmokeTest

object OpenDlssNrSmokeTest

CPU contracts and normal renderer startup with the optional backend explicitly disabled.

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

OpenDlssNrStudy.kt

An editable source image fed through the native neural-rendering (NR) pass, with a graded comparison.

OpenDlssNrStudy renders a source, either a fragment shader, a loaded image or a small geometry scene, into a texture, runs it through OpenDlssNrPass when OpenDlssNrSupport reports the native library ready, and displays a graded comparison. Without the library it still shows the source. OpenDlssNrStudy.reload swaps shaders live and OpenDlssNrStudy.saveFrame captures a frame.

src/jvmMain/kotlin/afield/graphics/render/neural/OpenDlssNrStudy.kt — read the source

OpenDlssNrStudy

class OpenDlssNrStudy(val renderer: Renderer, val size: Int = 768, val name: String = "nr-study", imagePath: String? = null, val geometry: Boolean = false, vertex: String = if(geometry)resource("cubes.vert") else defaultVertex, fragment: String = if(geometry)resource("cubes.frag") else defaultFragment, attach: Boolean = true) : Target, AutoCloseable

Editable source -> native NR -> graded comparison. Source mode works without the optional library. The current box run owns the study: stopping or rerunning it closes the old resources. The renderer also closes the study before destroying its device. The comparison and capture use the very same targets as the live Starting Point.

Declaration Summary
val renderer: Renderer
val size: Int
val name: String
val geometry: Boolean
val availability
val source
val pass
val output
val isClosed: Boolean Whether close has run, by the box or by the renderer going away.
var time
var animate
var recording The editable capture box reserves scene time while recording.
var controls
var grade
fun attach(): OpenDlssNrStudy
fun reload(vertex: String=if(geometry)resource("cubes.vert") else defaultVertex, fragment: String=if(geometry)resource("cubes.frag") else defaultFragment): Result<Unit> A shader edit replaces the source at a frame boundary. An edit that keeps the stage interface swaps the pipeline in place; one that changes it (adding or removing a varying, a push-constant member, a binding) rebuilds the source module and mesh. Invalid edits retain the current source.
val reloadPending: Boolean Whether the last interface-changing reload has reached the GPU.
fun saveFrame(path: Path): CompletableFuture<Path> Queue the next comparison frame for a lossless PNG after its GPU fence completes.
override fun render(command: CommandBuffer,index: Int)
override fun close()

OpenDlssNrSupport.kt

Whether the native neural-rendering (NR) pass can run on this renderer.

OpenDlssNrSupport.availability returns an OpenDlssNrSupport.Availability that is ready only when the device enabled the required Vulkan features and the pinned weights and kernels are on disk; its reason names the Gradle task to run when they are missing (fetchOpenDlssNrWeights, buildOpenDlssNrNative). The paths can be overridden with FIELD_OPEN_DLSS_NR_MODEL and FIELD_OPEN_DLSS_NR_SHADERS.

src/jvmMain/kotlin/afield/graphics/render/neural/OpenDlssNrSupport.kt — read the source

OpenDlssNrSupport

object OpenDlssNrSupport

Advertised features are checked before device creation; readiness also requires the pinned assets.

Declaration Summary
data class Availability(val ready: Boolean,val reason: String)
fun availability(renderer: Renderer): Availability

CappedTwistFixture.kt

A closed, rigged tube: the fixture for measuring what a twist costs a limb in volume.

RiggedFixture's bending cylinder is an open tube, so it encloses nothing and Manifold will not accept it as a solid. This is the same two-bone rig with both ends capped and its rings four times as close along the bone, so a 180° twist spreads over eight blended bands rather than one and the volume that goes missing is the blend's doing rather than the triangulation's. At rest the tube is a straight prism over a regular 12-gon, so its volume has a closed form and REST_VOLUME is exactly 3.

Nothing is committed for this rig: locate writes the generated text to a temporary file named after its hash, so a changed generator cannot be answered by a stale copy. A twisted limb, weighed and traced is this fixture posed, weighed and path-traced.

src/jvmMain/kotlin/afield/graphics/scene/CappedTwistFixture.kt — read the source

CappedTwistFixture

object CappedTwistFixture

A closed, rigged tube for measuring what skinning does to volume.

Declaration Summary
const val SEGMENTS: Int Radial segments around the tube.
const val RINGS: Int Rings along the tube, at y = 0, 0.25, … , 4.
const val RING_SPACING: Float Distance between rings.
const val RADIUS: Float Tube radius, the same as RiggedFixture.RADIUS.
const val HEIGHT: Float Tube height.
const val UPPER_JOINT_Y: Float Where boneUpper sits, in the bind pose.
const val MAX_TWIST_DEGREES: Float The twist, in degrees, at the end of the clip.
const val VERTEX_COUNT: Int Ring vertices plus the two cap centres.
const val TRIANGLE_COUNT: Int Side triangles plus two twelve-triangle cap fans.
const val REST_VOLUME: Double The prism's volume, in closed form: 12-gon area 0.75 times height 4. See the class comment.
fun secondsFor(degrees: Float): Float The clip time at which boneUpper has turned degrees about its own axis.
fun cappedTwistGltf(): String The generated glTF 2.0 document, as text. Deterministic: same bytes every call.
fun locate(): String An absolute path to the fixture, written from cappedTwistGltf into the temporary directory.

CompressedSplatPly.kt

Reads PlayCanvas's quantized .compressed.ply splat files back into ordinary per-splat floats.

Nothing upstream of this file has to know the format exists. readPlyAsFloatTable recognises the schema from the file's own header and routes here, and what comes back carries the columns an uncompressed 3D Gaussian splatting PLY would have given, in the same conventions. The unpacking itself is CompressedSplatPly: the per-chunk ranges, the bit layouts, and the inverse of each quantization. readRaw hands back the packed words instead, for a tool that wants to repack a file rather than draw it.

src/jvmMain/kotlin/afield/graphics/scene/CompressedSplatPly.kt — read the source

Top-level functions

Declaration Summary
fun readCompressedSplatPly(path: String, includeSphericalHarmonics: Boolean = false): PlyFloatTable Read a PlayCanvas .compressed.ply as the uncompressed splat table it stands for. See CompressedSplatPly for the format and CompressedSplatPly.readCompressedSplatPly for the parameters.

CompressedSplatPly

object CompressedSplatPly

The PlayCanvas compressed PLY — the .compressed.ply that splat-transform writes and that SuperSplat and the PlayCanvas engine read back.

Declaration Summary
const val CHUNK_SIZE Splats per chunk. Fixed by the format, not a tunable.
const val SH_C0 The DC term of the real spherical-harmonic basis, as 3DGS and splat-transform use it.
const val OPACITY_LOGIT_LIMIT Opacity is stored as an 8-bit sigmoid value, so 0 and 255 invert to a logit of -infinity and +infinity. The uncompressed PLY column this reader emits is the logit, and callers downstream push it back through a sigmoid — an infinity there is either a NaN or, in GaussianSplatRenderer's case, a non-finite guard that lands on 0.5 and turns a solid splat translucent. Clamping to ±30 costs nothing: sigmoid(30f) is 1 and sigmoid(-30f) is 9.4e-14, both of which round-trip to the byte they came from.
val requiredChunkProperties Chunk properties that every compressed PLY has, in the order the format declares them.
val colorChunkProperties Per-chunk colour range. Older files omit these six and store colour as a plain unorm instead of a lerp between them, so they are optional — but all six or none.
val vertexProperties The four packed 32-bit words that make up a splat.
val validShColumnCounts Coefficient counts a valid sh element may have: one, two or three bands beyond DC, times RGB.
val decompressedColumns Column names of the table readCompressedSplatPly produces, before any f_rest_*.
data class Layout(val splatCount: Int, val chunkCount: Int, val hasChunkColors: Boolean, val shColumns: List<String>) What a compressed PLY's header says about its payload, once recognised.
sealed interface Recognition The outcome of looking at a PLY header and asking "is this a compressed splat file?".
fun isCompressedSplatPly(path: String): Boolean True when path is a compressed splat PLY. Reads only the header. A file that is trying to be one and failing counts as one for this purpose — it is certainly not a plain PLY — so use readCompressedSplatPly when you want the reason.
fun rawCount(path: String): Int Read only the header and return the number of splats, without touching the packed payload.
fun readCompressedSplatPly(path: String, includeSphericalHarmonics: Boolean = false): PlyFloatTable Read a PlayCanvas compressed PLY and hand back the uncompressed splat table.
class RawSplats(val chunkRanges: FloatArray, val packed: IntArray, val count: Int, val hasChunkColors: Boolean) A file's quantized payload, read but not decoded: CHUNK_RANGE_COUNT range floats per chunk (the colour six left zero when the file omits them) and the four packed words per splat, in file order.
fun readRaw(path: String): RawSplats Read path's quantized payload without decoding it: one pass over the file, two arrays out and nothing per-splat but four getInts.
fun unpackUnorm(value: Int, bits: Int): Float Invert packUnorm: take the low bits of value back to 0..1.
fun dcFromColor(color: Float): Float A colour channel, as unorm, back to the SH DC coefficient the trainer produced.
fun logit(alpha: Float): Float A sigmoid'd opacity byte back to its logit, clamped to OPACITY_LOGIT_LIMIT so the 0 and 255 ends stay finite.
fun unpackShCoefficient(byte: Int): Float An SH coefficient byte back to its float, matching splat-transform's endpoint handling.
fun unpackRotationInto(value: Int, out: FloatBuffer) Undo the 2,10,10,10 quaternion packing straight into out as rot_0..rot_3.

CompressedSplatPlyFixture.kt

src/jvmMain/kotlin/afield/graphics/scene/CompressedSplatPlyFixture.kt — read the source

CompressedSplatPlyWriter.kt

How an edited splat cloud leaves Field, in the PlayCanvas .compressed.ply format it arrived in.

Box code reaches the object below through SplatCloud.write for a file and SplatCloud.toRawSplats for the packed tables a renderer uploads. Both run the same encoder over the same ordering, so what a preview shows and what a save contains are quantized alike. CompressedSplatPly is the reader this inverts.

Quantization is per chunk of 256 splats against that chunk's own bounds, which makes a splat's accuracy a question of how tightly its neighbours in file order bound it. That is what CompressedSplatPlyWriter.write's default Morton sort is for.

src/jvmMain/kotlin/afield/graphics/scene/CompressedSplatPlyWriter.kt — read the source

CompressedSplatPlyWriter

object CompressedSplatPlyWriter

Writes PlayCanvas .compressed.ply files — the inverse of CompressedSplatPly.

Declaration Summary
const val SCALE_LOG_LIMIT Log-scale clamp, matching splat-transform. Beyond this the exp is not worth storing.
fun write(file: File, cloud: SplatCloud, mortonSort: Boolean = true): Int Write cloud to file.
fun quantize(cloud: SplatCloud, mortonSort: Boolean = true): CompressedSplatPly.RawSplats Quantize cloud into the same payload CompressedSplatPly.readRaw would hand back after a round trip through write, without touching the disk.
const val MORTON_BITS Bits per axis in the Morton grid. See mortonOrder for why it is 13 and not 21.
fun mortonOrder(cloud: SplatCloud): IntArray Splat indices in Morton (Z-curve) order over a MORTON_BITS-per-axis grid across the cloud's bounds.

DualQuaternionSkinning.kt

The second of Field's two skinning blends: joint motions averaged as dual quaternions, for rigs that twist far enough to break the usual one.

Most callers arrive here without naming the file. GltfSkinning.skin is the entry point, and passing it SkinningOptions(method = SkinningMethod.DUAL_QUATERNION) hands the vertices straight to DualQuaternionSkinning.skin, which takes the same arguments and returns the same type. Linear blending averages the joint matrices, which is what every other engine defaults to and what the GPU path does by default; it holds up until two influences disagree by a large rotation, at which point the average of two rotations stops being a rotation and the cross-section of a twisted limb flattens. Averaging rigid motions instead keeps it round.

Three things qualify that, and DualQuaternionSkinning sets out each of them with its measurement. A dual quaternion is a rigid motion and nothing else, so a joint that scales or shears needs a residual matrix carried beside it. Two influences stored with opposite signs cancel rather than average, unless one of them is chosen as a pivot and the others are flipped to agree with it. And the comparison with linear blending is a trade rather than a win: this bulges a joint ring where linear blending pinches it.

src/jvmMain/kotlin/afield/graphics/scene/DualQuaternionSkinning.kt — read the source

DualQuaternionSkinning

object DualQuaternionSkinning

Dual quaternion linear blending — the skinning method that does not collapse a twisted joint.

Declaration Summary
const val RIGID_TOLERANCE: Float Below this, a residual 3×3 counts as the identity and the joint is treated as rigid.
class RigidSplit(val dq: DualQuaternionf, val residual: Matrix3f?) A palette matrix split into the rigid motion a dual quaternion carries exactly and the rest.
fun split(m: Matrix4f, tolerance: Float = RIGID_TOLERANCE): RigidSplit Factor m into a rigid motion and a residual 3×3, by Gram-Schmidt on the upper 3×3.
fun skin(positions: FloatArray, normals: FloatArray?, influences: GltfSkinning.VertexInfluences, palette: List<Matrix4f>, options: GltfSkinning.SkinningOptions = GltfSkinning.SkinningOptions()): GltfSkinning.SkinnedVertices Dual quaternion linear blending of positions and normals under influences.
fun candyWrapperRadius(positions: FloatArray, aroundY: Float, tolerance: Float = 0.25f, axis: Vector3f = Vector3f(0f, 1f, 0f)): Float The candy wrapper, as a number: the smallest distance from the bone axis of any vertex on the ring nearest aroundY, over positions.

GltfSkinning.kt

Skeletal animation on the CPU: bones, joint palettes and linear blend skinning, for a rigged file loaded through Assimp.

GltfSkinning reads the scene's node tree and its bones, samples one clip at a time, composes each joint's palette entry down the parent chain, and blends the vertices. The short way in is skinnedGltfToTriMesh: give it a file, a clip name and a time, and it returns the posed surface as a TriMesh in scene space, ready to measure, trace or write back out. CpuSkinner is the same maths against a live mesh, deforming into the vertex buffer the renderer uploads.

GpuSkinning carries the palette to the GPU and blends in the vertex shader, which is what a crowd needs, and takes this file as its oracle. Where linear blending fails is a twist about a bone's own axis, which pinches the joint ring to nothing: DualQuaternionSkinning is the alternative blend, and A twisted limb, weighed and traced weighs the difference between the two.

src/jvmMain/kotlin/afield/graphics/scene/GltfSkinning.kt — read the source

GltfSkinning

object GltfSkinning

Skeletal animation: bones, joint palettes and linear blend skinning, on the CPU.

Declaration Summary
const val MAX_INFLUENCES: Int Influences kept per vertex. glTF's own baseline, and what every GPU skinning path assumes.
class SkeletonNode(val name: String, val parent: Int, val local: Matrix4f) One node of the scene hierarchy, in the rest pose.
class Bone(val name: String, val node: Int, val inverseBind: Matrix4f) A node some mesh weights vertices to, with the matrix that takes mesh space into its bind space.
class JointChannel(val node: Int, val samples: List<TRSSample>) One animated node within a clip: the node's local TRS, sampled at AnimationClip.sampleRate.
class AnimationClip(val name: String, val durationSeconds: Float, val sampleRate: Float, val channels: List<JointChannel>) A named clip: for each animated node, its local TRS sampled on a uniform grid.
class Skeleton(val nodes: List<SkeletonNode>, val bones: List<Bone>, val clips: List<AnimationClip>) The hierarchy, the bones and the clips — everything needed to build a joint palette at a time.
class VertexInfluences(val vertexCount: Int, val jointIndex: IntArray, val weight: FloatArray, val discardedInfluences: Int, val largestDiscardedWeight: Float) Per-vertex joint indices and weights, four-wide and flat.
class MeshSkin(val skeleton: Skeleton, val influences: VertexInfluences) A mesh's skin: which skeleton, and how its vertices attach to it.
enum class SkinningMethod How skin averages the influences of a vertex that several joints pull on.
data class SkinningOptions(val method: SkinningMethod = SkinningMethod.LINEAR_BLEND, val exactNormalMatrix: Boolean = true, val renormalize: Boolean = true) Knobs on skin.
class SkinnedVertices(val vertexCount: Int, val position: FloatArray, val normal: FloatArray?, val singularNormalMatrices: Int, val nonRigidJoints: Int = 0, val antipodalFlips: Int = 0) What skin produced: deformed positions and normals, plus what it had to notice on the way.
fun skin(positions: FloatArray, normals: FloatArray?, influences: VertexInfluences, palette: List<Matrix4f>, options: SkinningOptions = SkinningOptions()): SkinnedVertices Linear blend skinning: positions and normals deformed by palette under influences.
fun readNodes(scene: AIScene): List<SkeletonNode> Walk the whole scene's node tree into a flat pre-order list with parent indices.
fun readClips(scene: AIScene, nodes: List<SkeletonNode>, sampleRate: Float): List<AnimationClip> Every animation in the file, resampled onto a uniform grid at sampleRate Hz.
fun readMeshSkin(mesh: AIMesh, nodes: List<SkeletonNode>, clips: List<AnimationClip>): MeshSkin? The bones of one Assimp mesh, plus its per-vertex influences.
fun sampleAt(samples: List<TRSSample>, seconds: Float): TRS The TRS at seconds in a uniformly-sampled channel; clamped at both ends, lerp/slerp between.

GpuSkinning.kt

Skinning in the vertex shader: one mesh, one draw call, and a crowd of characters each posed differently.

The bind-pose vertices are uploaded once and never touched again. Per frame the CPU writes only the joint palette, which is instances times bones matrices, and the GPU does the per-vertex blend, so a hundred characters cost a hundred palettes rather than a hundred deformations. GpuSkinner is the box-facing half: build one over a loaded GltfSupport.MeshInstance, hand it a palette per instance with uploadPalettes, then bindTo the mesh you are drawing. GpuSkinning holds the GLSL and the palette packing underneath it.

The CPU path in GltfSkinning is the oracle. The same GLSL that draws also runs as a compute shader writing deformed positions to a buffer the CPU reads back, so a test that compares the two is checking the shader that renders rather than a second implementation written to pass it. Linear blending and dual quaternions can be mixed inside one draw call, chosen per instance, which is the clearest way to see what separates them.

src/jvmMain/kotlin/afield/graphics/scene/GpuSkinning.kt — read the source

GpuSkinning

object GpuSkinning

Skinning in the vertex shader, with a joint-matrix palette on the GPU, and a crowd of characters from one mesh and one draw call.

Declaration Summary
const val MAX_INFLUENCES: Int Influences per vertex, fixed by GltfSkinning.MAX_INFLUENCES and by uvec4/vec4.
const val VERTEX_STRIDE_FLOATS: Int Floats per vertex in the packed buffer the bundled glTF shaders read.
const val LOCAL_SIZE: Int Invocations per workgroup in verifyComputeShader.
const val DUAL_VEC4S_PER_JOINT: Int vec4s per joint in the dual-quaternion palette: real.xyzw, dual.xyzw, then the residual 3×3 S as three columns in JOML order (m00 m01 m02, …) with w unused.
const val DUAL_FLOATS_PER_JOINT: Int Floats per joint in the dual-quaternion palette: DUAL_VEC4S_PER_JOINT × 4.
const val METHOD_LINEAR: Int Method word 0: linear blend skinning from the mat4 palette.
const val METHOD_DUAL_QUATERNION: Int Method word bit 0: dual quaternion blending from the dual palette.
const val METHOD_RESIDUAL_BIT: Int Method word bit 1: this instance's palette has at least one non-rigid joint, so the residual 3×3 must be blended. Only meaningful with METHOD_DUAL_QUATERNION.
fun packDualPalette(palette: List<Matrix4f>, into: FloatArray, offset: Int = 0): Int Write palette into into at offset in the dual-quaternion layout — DUAL_FLOATS_PER_JOINT floats per joint — and return the method word the shader needs for it.
fun glsl(): String The skinning maths, as GLSL: five buffer-reference types and six functions.
val skinnedVertexShader: String GltfSupport.gltVertexShader with the skin in it: same packed 12-float vertex, same buffer references, same push constants — plus a joint stream, a palette and gl_InstanceIndex.
val skinnedFragmentShader: String A fragment shader that actually uses vColor.
val verifyComputeShader: String The same glsl, run over every vertex of every instance, writing deformed positions and normals to a buffer the CPU can read back and compare with GltfSkinning.skin.

GpuSkinner

class GpuSkinner(val instance: GltfSupport.MeshInstance, val instances: Int = 1, val renderer: Renderer = fromContext(), val name: String = "gpuSkin")

The GPU half of CpuSkinner: joints and weights uploaded once, a palette written per frame, and a crowd drawn from one mesh.

Declaration Summary
val instance: GltfSupport.MeshInstance
val instances: Int How many characters this palette is sized for. One draw call covers all of them.
val renderer: Renderer
val name: String Distinguishes this skinner's buffers from another's.
val skin: GltfSkinning.MeshSkin The mesh's skin; non-null by construction, as for CpuSkinner.
val vertexCount: Int
val boneCount: Int
val clips: List<String> Clip names available on this mesh's skeleton, in file order.
val bindPositions: FloatArray The bind-pose positions and normals, snapshotted at construction.
val bindNormals: FloatArray
val jointBuffer: FrameBufferGroup uvec4 per vertex. Static: written once in init, never again.
val weightBuffer: FrameBufferGroup vec4 per vertex. Static.
val paletteBuffer: FrameBufferGroup instances × boneCount mat4. The only thing written per frame, and the reason a crowd is nearly free: 64 characters of a 7-bone rig is 448 matrices, against 64 × vertexCount vertices the GPU would otherwise have to be handed.
val dualPaletteBuffer: FrameBufferGroup The same instances × boneCount palette as dual quaternions plus residual — GpuSkinning.DUAL_FLOATS_PER_JOINT floats a joint. Written by uploadPalettes only for the instances that are dual-quaternion skinned; the rest of it is never read.
val methodBuffer: FrameBufferGroup One uint per instance: which blend the shader uses for it. Written by uploadPalettes.
var method: GltfSkinning.SkinningMethod The blend every instance gets unless uploadPalettes is handed a per-instance list. Linear blend by default, matching GltfSkinning.SkinningOptions and every other engine.
fun placedPalette(model: Matrix4f, clip: String?, seconds: Float): List<Matrix4f> This instance's palette at seconds of clip, with model multiplied in on the left, so one entry is model · global(node, t) · inverseBind.
fun blendedPalette(model: Matrix4f, clipA: String?, aSeconds: Float, clipB: String?, bSeconds: Float, mix: Float): List<Matrix4f> As placedPalette, but blending two clips — the per-instance clip blend. mix is 0 for all clipA, 1 for all clipB.
fun uploadPalettes(palettes: List<List<Matrix4f>>, methods: List<GltfSkinning.SkinningMethod> = List(palettes.size) { method }) Write one palette per instance. palettes.size must be at most instances, and every entry must have boneCount matrices.
fun methodWords(count: Int = instances): IntArray The method word the shader will see for each of the first count instances after the last uploadPalettes — GpuSkinning.METHOD_LINEAR, or GpuSkinning.METHOD_DUAL_QUATERNION with GpuSkinning.METHOD_RESIDUAL_BIT when that palette had a non-rigid joint.
fun uploadPalette(palette: List<Matrix4f>) One palette, for the common single-character case.
fun bindTo(mesh: Mesh, camera: Camera = fromContext(), instanceCount: Int = instances, tint: Boolean = instances > 1) Point mesh at this skinner and set every push constant GpuSkinning.skinnedVertexShader needs. Call once; after this only uploadPalettes and, if the camera moves, updateCamera are per-frame.
fun updateCamera(mesh: Mesh, camera: Camera = fromContext()) Push the camera. modelView is view alone, with no model matrix: glTF 2.0 §3.7.3.2 says a skinned mesh node's own transform is ignored, and each instance's placement is already inside its palette. Composing a model matrix in here would apply it twice.
fun uploadPosedVertices(mesh: Mesh) Hand mesh back to the stock, un-skinned path: the bind pose as instance.buffers holds it now, drawn by the skinEnabled == 0 branch.
fun restoreBindPose(mesh: Mesh) Put the bind pose back in the GPU buffer and re-enable GPU skinning.

GpuSkinningEvidence.kt

A program that draws GPU-skinned characters through the real Vulkan path and writes the frames out as PNGs, each one paired with a CPU-skinned frame of the same pose.

./gradlew --no-daemon runGpuSkinningEvidence

It opens a window, poses a rigged fixture bent, blended across two clips, as a crowd of instances and twisted, and writes the pictures under dev_docs/agents/evidence/gpu_skinning. Each pose is rendered twice so that the two frames can be differenced: they differ in where the skinning happened and in nothing else, which makes the difference a statement about the vertex shader rather than about two renderers. A zero difference is weak on its own, so a control frame with skinning switched off has to look different from the skinned one.

Every check ends the run with a non-zero exit code when it fails, including a frame that came back blank, so this reads as a gate as well as a set of pictures. SkinningEvidence is the CPU-side companion, drawn with the software rasteriser.

src/jvmMain/kotlin/afield/graphics/scene/GpuSkinningEvidence.kt — read the source

GpuSkinningEvidence

object GpuSkinningEvidence

Render GPU-skinned characters and write frames a person can look at.

Declaration Summary
const val EVIDENCE_DIR: String
fun main(args: Array<String>)

LoadPly.kt

Reads one element of a PLY file as a flat table of floats, addressed by property name.

This is the column-oriented half of Field's PLY support, and the Gaussian splat path reads through it: gaussianSplats and SplatCloud.read both arrive at readPlyAsFloatTable, then ask for the columns they want by name. Every scalar property is widened to a float whatever it was on disk, and list properties are read past and dropped, so a mesh's faces do not survive the trip. For triangles, and a module to draw them with, load through PlySupport.loadPly instead.

A compressed PlayCanvas splat file is recognised from its header and decompressed on the way through, so a caller sees the same columns either way; see CompressedSplatPly.

src/jvmMain/kotlin/afield/graphics/scene/LoadPly.kt — read the source

Top-level functions

Declaration Summary
fun readPlyAsFloatTable(path: String, elementName: String = "vertex", includeSphericalHarmonics: Boolean = false): PlyFloatTable Read one PLY element into a flat PlyFloatTable, one row per element, every scalar property widened to a float.
fun testPly(fn: String): PlyFloatTable Reads a file and prints its columns, row count and float count. A look at a PLY, from a box.

PlyFloatTable

data class PlyFloatTable(val offsets: Map<String, Int>, val stride: Int, val buffer: FloatBuffer)

One PLY element as rows of floats: a shared buffer, a stride in floats, and the column each property name sits at.

Declaration Summary
val offsets: Map<String, Int> Column index per property name, in the order the file's header declared them.
val stride: Int Floats per row, which is the number of scalar properties the element had.
val buffer: FloatBuffer Every row end to end, rowCount × stride floats.
val columns: List<String> The property names, in column order.
val rowCount: Int How many of the element's entries the buffer holds. Throws if its length is not a whole number of rows.
fun hasColumn(name: String): Boolean Whether the element carried this property, which is how an optional column is tested for.
fun columnOffset(name: String): Int This property's column index, or an error naming every column the file does have.
fun floatAt(row: Int, field: String): Float One value, by row and property name. Bounds-checked on the row, and throws on an unknown name.
fun floatAtOrNull(row: Int, field: String): Float? The same, returning null for a column the file does not have rather than throwing.
fun firstFloatAtOrNull(row: Int, fields: Iterable<String>): Float? The first of these property names the file has. PLY spells some quantities more than one way, colour as red or diffuse_red and transparency as alpha or diffuse_alpha, and this takes whichever of a list is present.
fun requireColumns(vararg fields: String): PlyFloatTable Returns this table if it has all of these columns, and otherwise throws naming both the missing ones and the columns the file does have. The splat readers call it as the first thing they do with a table, so a wrong file fails by name rather than by arithmetic.
fun subsetFloatBufferColumns(selectedFields: List<String>): FloatBuffer A fresh buffer holding only these columns, in the order asked for, packed to that narrower stride. Nothing in the tree calls this yet; it is here for narrowing a wide table down to the few columns a GPU buffer wants.

MeshFromDisk.kt

Connects the loaders that read geometry off disk to the CPU renderer that can look at it.

Field has four things that read geometry from a file, and one of them feeds the CPU preview:

  • afield.geometry.mesh.mesh_io.kt reads OBJ and STL straight into TriMesh — the type afield.geometry.mesh.showMesh, TriMesh.renderPng and the inline toHTML() card all render.
  • PlySupport reads PLY (ASCII, binary, both endians, colours, normals, point clouds) into a packed 10-float GPU buffer.
  • GltfSupport reads glTF/GLB — and, being Assimp.aiImportFile underneath, also FBX, DAE, 3DS, OBJ, STL and PLY — into a packed 12-float GPU buffer plus materials.
  • afield.graphics.scene.fabricated.FabricateGltfBridge takes that same model into the PBR StandardRenderer.

Both GPU loaders produce plain host memory — memAlloc, not a Vulkan allocation — which is what lets their output be read back into a TriMesh on the CPU: PlySupport.PlyModel.toTriMesh, assimpToTriMesh and the dispatcher meshFromAnyFile, plus a Box.showMesh overload that takes a File.

showMesh(File("/path/to/thing.glb"))                     // straight to the orbitable viewer
val m = meshFromAnyFile("/path/to/scan.ply")             // or keep the mesh and compute with it
println(m.summary())

They live here rather than in afield.stdlib because mesh_io.kt carries no LWJGL dependency and these need one. afield.graphics.scene.* is in every box's default imports (see afield.document.graph.intro), so a box can call them without an import anyway.

What is not here, because nothing in the tree can read it: STEP / IGES / BREP. There is no CAD kernel on the classpath, and those formats need NURBS-to-mesh tessellation rather than a parser — see public/docs/geometry_from_disk.md for the full list of what does and does not load.

src/jvmMain/kotlin/afield/graphics/scene/MeshFromDisk.kt — read the source

Used in: Geometry from disk

Top-level functions

Declaration Summary
fun PlySupport.PlyModel.toTriMesh(keepNormals: Boolean = true, keepColors: Boolean = true): TriMesh This PLY model's packed vertex buffer, unpacked into a TriMesh.
fun assimpToTriMesh(path: String, keepColors: Boolean = true): TriMesh Read any file Assimp can import — glTF, GLB, FBX, DAE, 3DS, OBJ, STL, PLY and the rest of its forty-odd formats — into one TriMesh.
fun meshFromAnyFile(path: String): TriMesh Load a mesh from a path, in any format Field can read, and return it as a TriMesh you can render, measure, weld, decimate or write back out.
fun Box.showMesh(path: String, channel: String = "main", view: MeshView = MeshView(), caption: String? = null, title: String? = File(path).name, renderer: MeshRenderer = MeshRenderer.GPU, height: Int? = null) Read a mesh file and push a rendered view of it to one of this box's output channels — the one-liner for "I have a mesh on disk, show me".
fun Box.showMesh(file: File, channel: String = "main", view: MeshView = MeshView(), caption: String? = null, title: String? = file.name, renderer: MeshRenderer = MeshRenderer.GPU, height: Int? = null) showMesh from a File rather than a path string, with the same renderer choice. java.io is not in a box's default imports, so inside a box the String overload is usually the one you want.

PlyLoaderUnitTest.kt

Twelve checks over Field's PLY readers, as a standalone program that needs no Vulkan device.

It writes its own files into a temporary directory and reads them back: ASCII and binary meshes in both byte orders, a point cloud, a file carrying neither colours nor normals, the float-table view (PlyFloatTable), and the compressed Gaussian-splat format. One of those compressed files has its packed words written out as literal bit patterns, so the reader is measured against the format rather than against a second implementation of it; the others are round-tripped through the writer.

Each check prints a line and the run ends in All tests PASSED, which is what the smoke catalog matches on as four.ply-loader.smoke.

src/jvmMain/kotlin/afield/graphics/scene/PlyLoaderUnitTest.kt — read the source

Top-level functions

Declaration Summary
fun main() Runs PlyLoaderUnitTest.runTests and exits 0 when every check passed, 1 otherwise.

PlyLoaderUnitTest

object PlyLoaderUnitTest

Unit tests for PLY loader that don't require graphics context

Declaration Summary
fun createTestFiles() Write the small PLY files the checks read, into ply_test under the system temporary directory. runTests calls this itself; call it directly to leave the files somewhere a viewer can open them.
fun runTests(): Boolean Run every check, printing a pass or fail line for each, and return whether all of them passed. A failing check does not stop the others, so one run reports every problem it finds.

RiggedFixture.kt

The rigged glTF files the skinning tests pose, generated by this source rather than downloaded.

The repository ships no rigged model, and skinning cannot be tested without one, so these two write their own: glTF 2.0 text with the buffer inline as a base64 data URI, the same bytes on every call. RiggedFixture is the well-behaved case, a two-bone bending cylinder whose constants are chosen so the bind pose comes out exact in binary, which lets a test assert bit-identity against the unskinned mesh instead of a tolerance. StressFixture is the awkward one: a joint that scales instead of rotating, so it shears a tilted plate; three vertices carrying six influences where only four fit; and a skinned mesh node with a transform that glTF says to ignore. Each of those arrives with the number it should produce.

Both commit their asset under dev_docs/agents/fixtures/skinning/, and locate returns an absolute path to it from wherever the server happens to be running. CappedTwistFixture is the third rig, a closed tube for measuring volume.

src/jvmMain/kotlin/afield/graphics/scene/RiggedFixture.kt — read the source

RiggedFixture

object RiggedFixture

A rigged glTF, generated rather than downloaded.

Declaration Summary
const val SEGMENTS: Int Radial segments around the tube.
const val RINGS: Int Rings along the tube, at y = 0 .. RINGS - 1.
const val RADIUS: Float Tube radius.
const val UPPER_JOINT_Y: Float Where boneUpper sits, in the bind pose.
const val VERTEX_COUNT: Int Vertices in the generated mesh.
const val TRIANGLE_COUNT: Int Triangles in the generated mesh.
fun upperWeight(y: Float): Float The weight this vertex gives to boneUpper; 1 - that goes to boneLower.
fun bendingCylinderGltf(): String The generated glTF 2.0 document, as text. Deterministic: same bytes every call.
const val COMMITTED_PATH: String Where the committed copy lives, relative to the repository root.
fun locate(): String An absolute path to the committed fixture, found by walking up from the working directory.
fun main(args: Array<String>) Rewrite the committed asset from this source. Run from the repository root.

StressFixture

object StressFixture

The second, adversarial rig: a shearing plate and a vertex with more influences than fit.

Declaration Summary
const val COMMITTED_PATH: String Where the committed copy lives, relative to the repository root.
val BIND_NORMAL: FloatArray The plate's bind-pose normal: the plane is spanned by +x and (0, 1, -1)/√2.
val shearedNormal: FloatArray The plate's normal after shearBone reaches scale (1, 2, 1), by hand.
const val CAPPED_DISPLACEMENT: Float What the six-influence triangle's x displacement is with the four-wide cap: 2.0 / 0.9.
const val UNCAPPED_DISPLACEMENT: Float And without it: 0.30·1 + 0.25·2 + 0.20·3 + 0.15·4 + 0.06·5 + 0.04·6.
val SIX_WEIGHTS: FloatArray The six weights, largest first, as the file writes them.
fun shearAndCapGltf(): String The generated glTF 2.0 document, as text. Deterministic: same bytes every call.
fun locate(): String An absolute path to the committed fixture; see RiggedFixture.locate.

SkinnedMeshFromDisk.kt

Posing a rigged glTF on the CPU: into a fresh TriMesh, or back into the vertex buffer a loaded model already owns.

Which of the two you want depends on what happens next. skinnedGltfToTriMesh loads the file, deforms it at one time and hands back geometry, which suits measuring a pose, feeding the software rasteriser, or treating a frame as data. CpuSkinner wraps a mesh that is already loaded for drawing and overwrites its positions and normals in place, so the stock glTF shaders draw a moving character without knowing that anything is being skinned.

posedGltfToTriMesh is skinnedGltfToTriMesh with the joint palette supplied by the caller, for a clip blend or a pose an IK solver produced, and gltfClipNames reads the clip names out of a file without deforming anything.

The blend itself is GltfSkinning's; what this file adds is the plumbing on either side of it. GpuSkinner moves the same work into the vertex shader, which is the path to take when the character has to animate every frame.

src/jvmMain/kotlin/afield/graphics/scene/SkinnedMeshFromDisk.kt — read the source

Top-level functions

Declaration Summary
fun skinnedGltfToTriMesh(path: String, clip: String? = null, seconds: Float = 0f, keepColors: Boolean = true, options: GltfSkinning.SkinningOptions = GltfSkinning.SkinningOptions()): TriMesh A rigged file, deformed at a time, as a TriMesh you can render, measure and look at.
fun posedGltfToTriMesh(path: String, keepColors: Boolean = true, options: GltfSkinning.SkinningOptions = GltfSkinning.SkinningOptions(), palette: (GltfSkinning.Skeleton) -> List<Matrix4f>): TriMesh The same, with the palette chosen by the caller — for a clip blend, a pose driven by an IK solver, or anything else that is not "clip x at time t".
fun gltfClipNames(path: String): List<String> The clip names in a rigged file, in file order. Empty for a file with no animations.

CpuSkinner

class CpuSkinner(val instance: GltfSupport.MeshInstance, val options: GltfSkinning.SkinningOptions = GltfSkinning.SkinningOptions())

CPU skinning straight into the GPU vertex buffer a loaded MeshInstance already owns — so the bundled glTF shaders draw a deforming character with no shader change at all.

Declaration Summary
val instance: GltfSupport.MeshInstance
val options: GltfSkinning.SkinningOptions
val skin: GltfSkinning.MeshSkin The mesh's skin; non-null by construction.
val clips: List<String> Clip names available on this mesh's skeleton, in file order.
fun pose(palette: List<Matrix4f>): GltfSkinning.SkinnedVertices Deform to palette — one matrix per entry of skin.skeleton.bones — and write the result into the instance's vertex buffer, overwriting only the position and normal fields.
fun pose(clip: String?, seconds: Float): GltfSkinning.SkinnedVertices Deform to clip clip at seconds. null is the rest pose.
fun reset() Put the bind pose back, bit for bit — the snapshot is written straight through.

SkinningEvidence.kt

The evidence run behind Field's CPU glTF skinning: ./gradlew --no-daemon runSkinningEvidence.

It writes the rigged fixture out as glTF, loads it back through the real Assimp path, and prints what came back with the population each number was measured over: the skeleton and its clips, the influence weights after the four-wide cap, whether the bind pose is bit-identical to the unskinned mesh, and the displacement through the bend. A second, harder fixture covers the cases that are easy to get wrong: a translated node that glTF says to ignore for a skinned mesh, a shear whose correct vertex normal is known in closed form, and a vertex with six influences for the cap to discard from. The run ends with a throughput figure in nanoseconds per vertex, with and without exact inverse-transpose normals.

Frames go to dev_docs/agents/evidence/skinning/ for a person to look at, named by SkinningEvidence.frames so the sequence reads in sorted order, and the numbers are the ones dev_docs/SKINNED_CHARACTERS.md quotes. GpuSkinningEvidence is the same exercise for the compute-shader path.

src/jvmMain/kotlin/afield/graphics/scene/SkinningEvidence.kt — read the source

SkinningEvidence

object SkinningEvidence

Regenerate the rigged fixture, measure the skinning, and write frames a person can look at.

Declaration Summary
const val FIXTURE: String
const val EVIDENCE_DIR: String
fun anchor(minX: Float = -3f, minY: Float = -0.5f, minZ: Float = -2f, maxX: Float = 1.5f, maxY: Float = 4.5f, maxZ: Float = 2f): TriMesh Two 2cm triangles at opposite corners of a fixed box, so every frame in a sequence is framed identically. Invisible at the sizes here, and outside the cylinder so they never occlude it.
fun main(args: Array<String>)
fun frames(): List<Pair<String, () -> TriMesh>> The frames written by main, named so the sequence reads in sorted order.

simpleGLTFSupport.kt

Loading a model file onto the GPU, with its materials and textures.

gltfModelHere("model.glb") { } in a box is the usual case: the call expands the way moduleHere does, into a graphics module carrying the bundled glTF shaders at the end of this file, and hands back that module together with the loaded GltfSupport.GltfModel. GltfSupport.loadGltfBasic is the same load without a module, for code bringing its own shaders.

The name says glTF and the importer is Assimp, so the formats are Assimp's: glTF and GLB, and also FBX, DAE, OBJ, STL and PLY. A load returns one GltfSupport.MeshInstance per mesh in the node tree, each carrying host-side vertex and index buffers in the fixed 12-float layout those shaders read (position, normal, uv, colour), its material, and the node's animation track resampled at a fixed rate, which this file's sample extension interpolates at whatever time you ask for.

Two things catch callers out. GltfSupport.GltfLoadConfig's loadTextures is on by default and that path asks the box context for a Renderer, so a load taking the default arguments cannot run outside a live renderer; pass GltfLoadConfig(loadTextures = false) for geometry alone, as assimpToTriMesh does. And a GltfSupport.MeshInstance's bindPose is a translation/rotation/scale triple, which cannot hold the mirror or shear a glTF node transform is allowed to carry; bindMatrix, documented at length below, is the full 4×4 the loader also keeps.

src/jvmMain/kotlin/afield/graphics/scene/simpleGLTFSupport.kt — read the source

Used in: Geometry from disk

Top-level functions

Declaration Summary
fun Box.gltfModelHere(p: Parameters.() -> Unit): Pair<ModulePointer, List<GltfSupport.GltfModel>> gltfModelHere("model.glb") { } in box code: a graphics module for that file, carrying the bundled glTF shaders, together with the model the importer read.
fun Box.macro_modelHere(args: List<String>): Insert?
fun Box.macro_modelWithTextureHere(args: List<String>): Insert?
fun Box._gltfModelHere(filename: String, p: Parameters.() -> Unit = {}): Pair<ModulePointer, List<GltfSupport.GltfModel>>
fun Box._gltfModelHereWithTexture(filename: String, p: Parameters.() -> Unit = {}): Pair<ModulePointer, List<GltfSupport.GltfModel>>
fun registerGLTFLoader()
fun List<TRSSample>.sample(time: Float): TRS?

GltfSupport

object GltfSupport

Declaration Summary
data class TRS(val t: Vector3f, val r: Quaternionf, val s: Vector3f) : HasMatrix4f
data class TRSSample(val time: Float, val trs: TRS)
data class MeshBuffers(val vertexData: java.nio.ByteBuffer, val indexData: java.nio.IntBuffer, val vertexCount: Int, val indexCount: Int, val tangentData: java.nio.ByteBuffer? = null)
data class TextureSlot(val type: Int, val uvSet: Int, val texture: LoadedTexture, val valid: Boolean, val transform: TextureTransform? = null)
data class TextureTransform(val offsetX: Float = 0f, val offsetY: Float = 0f, val scaleX: Float = 1f, val scaleY: Float = 1f, val rotation: Float = 0f, val texCoord: Int? = null) A glTF KHR_texture_transform: uv' = offset + rotate(rotation) * (scale * uv), in glTF UV space. Read off the file rather than Assimp, whose UV-transform pivot and order differ.
data class MaterialTextured(val baseColor: Vector4f, val textures: List<TextureSlot> = emptyList(), val name: String = "", val metallicFactor: Float = 1f, val roughnessFactor: Float = 1f, val clearcoatFactor: Float = 0f, val clearcoatRoughnessFactor: Float = 0f, val transmissionFactor: Float = 0f, val alphaMode: String = "OPAQUE", val alphaCutoff: Float = 0.5f, val doubleSided: Boolean = false)
data class MeshInstance(val name: String, val meshIndex: Int, val buffers: MeshBuffers, val bindPose: TRS, val material: MaterialTextured, val doTextures: Boolean = false, val animationSamples: List<TRSSample>?)
data class GltfModel(val meshes: List<MeshInstance>)
data class GltfLoadConfig(val sampleRate: Float = 60f, val includeVertexColors: Boolean = true, val loadTextures: Boolean = true, val loadSkins: Boolean = true) Config: - sampleRate: Hz for animation sampling (if any) - includeVertexColors: when true, always writes RGBA per-vertex. - loadTextures: when true, collect material textures (diffuse/baseColor/normal/etc.)
fun loadModel(path: String)
fun loadGltfBasic(path: String, config: GltfLoadConfig = GltfLoadConfig()): GltfModel
val gltVertexShader
val gltfFragmentShader
val gltfFragmentShaderTextured

simplePLYSupport.kt

Loads a PLY mesh or point cloud into a live Vulkan module, shaders included.

In a box, plyModelHere("model.ply") is the whole of it: the macro builds a module wired to the bundled shaders, loads the file, and returns both the module and the loaded model. Point clouds take plyPointCloudHere, which draws every point as a screen-aligned quad that the fragment shader rounds off into a dot. Underneath, PlySupport is a plain reader, ASCII or binary PLY in either endianness, and it will load a file with no renderer in sight.

For a TriMesh to compute with rather than a module to draw, meshFromAnyFile reads the same files; see Geometry from disk. Gaussian splats are the other direction: their PLY carries a rotation and a scale per splat rather than faces, and readPlyAsFloatTable is what reads it.

src/jvmMain/kotlin/afield/graphics/scene/simplePLYSupport.kt — read the source

Used in: Geometry from disk

Top-level functions

Declaration Summary
fun Box.plyModelHere(p: Parameters.() -> Unit): Pair<ModulePointer, PlySupport.PlyModel> What a box writes to draw a PLY mesh: plyModelHere("model.ply") { }, returning the module and the loaded model.
fun Box.plyPointCloudHere(p: Parameters.() -> Unit): Pair<ModulePointer, PlySupport.PlyModel> The same for a point cloud, drawing each point as a rounded quad. Rewritten to _plyPointCloudHere.
fun Box.macro_plyModelHere(args: List<String>): Insert? Expands a plyModelHere call, attaching the bundled mesh shaders to the module it creates.
fun Box.macro_plyPointCloudHere(args: List<String>): Insert? Expands a plyPointCloudHere call, attaching the bundled point-cloud shaders instead.
fun Box._plyModelHere(filename: String, p: Parameters.() -> Unit = {}): Pair<ModulePointer, PlySupport.PlyModel> What plyModelHere becomes: a module from p, plus filename loaded through PlySupport.loadPly.
fun Box._plyPointCloudHere(filename: String, pointSize: Float = 8.0f, p: Parameters.() -> Unit = {}): Pair<ModulePointer, PlySupport.PlyModel> What plyPointCloudHere becomes. The module draws triangles rather than points, because the point-cloud vertex shader pulls six vertices per point to make a screen-aligned quad.
fun registerPLYLoader() Registers both call rewrites with the macro registry at startup.

PlySupport

object PlySupport

Self-contained PLY (Polygon File Format) loader for Field 4.

Declaration Summary
data class PlyMeshBuffers(val vertexData: ByteBuffer, val indexData: java.nio.IntBuffer?, val vertexCount: Int, val indexCount: Int, val hasColors: Boolean, val hasNormals: Boolean, val isPointCloud: Boolean) PLY mesh buffers containing vertex and optional index data.
data class PlyModel(val buffers: PlyMeshBuffers, val name: String)
fun loadPly(path: String): PlyModel
val plyVertexShader
val plyFragmentShader
val plyPointCloudVertexShader
val plyPointCloudFragmentShader

Introspection.kt

Reflection of a compiled shader's interface: the uniform blocks, storage buffers, textures, images, acceleration structures and push constants it declares, with their sets, bindings, member offsets and sizes.

Introspection compiles GLSL to SPIR-V through ShaderSPIRVUtils (or takes SPIR-V or Slang reflection directly, through fromSpirv and fromSlangReflection) and reads the result with SPIRV-Cross, so the layout it reports is the compiler's, not a parse of the source. Introspected holds the reflected types; parseGlslMemberComments recovers the trailing comments on block members, which the reflection does not carry. Field's modules use this to allocate and bind each shader's buffers.

src/jvmMain/kotlin/afield/graphics/shader/Introspection.kt — read the source

Top-level functions

Declaration Summary
fun parseGlslMemberComments(source: String): Map<String, String> Scrape the // trailing comment off every member declaration inside a GLSL block — the push-constant block, a struct, a UBO, an SSBO — so a human note written next to a uniform can travel to the Kotlin accessor generated for it (see afield.graphics.shader.insertForShader, which appends it to the generated preamble line, and afield.tools.docs.ShaderBindingDocs, which reads it back for the completion popup).
fun Either<PrimitiveType, List<Introspected.Member>>.layoutSizeInBytes(): Int Byte size of a member's type: a primitive's size, or the maximum offset+size across nested members. Structure.kt sizes a buffer's members with it.
fun Introspected.Member.layoutSizeInBytes(): Int Byte size of a member: a device pointer's size if physical-pointer, else its type's layout size.
fun List<Introspected.Member>.membersLayoutSizeInBytes(): Int Byte size of a list of members: the furthest offset+size any of them reaches, so a block's size is its last member's end rather than the sum of its members.

ImageType

enum class ImageType(val vulkan_format: Int)

Constants: rgba16f.

PrimitiveType

enum class PrimitiveType(val components: Int, val componentsSizeInBytes: Int, val bufferFormat: Int, val kotlinTypeName: String, val kotlinDefault: String, val writeXToBuffer: String)

Constants: float, vec2.

Declaration Summary
val components: Int How many scalars the type holds. Matrices count every element, so mat4 is 16 rather than 4 columns.
val componentsSizeInBytes: Int Bytes per scalar: 4 for the 32-bit types, 2 for the f16 ones, 8 for the 64-bit ones.
val bufferFormat: Int The VK_FORMAT_* a vertex attribute of this type is described with, and -1 for the matrix and 64-bit types, which no single vertex format covers. Nothing substitutes a valid format for that -1.
val kotlinTypeName: String The fully-qualified Kotlin type a generated preamble declares for this, so a vec3 uniform arrives as an afield.math.linalg.Vector3f.
val kotlinDefault: String The expression a generated declaration is initialised with. vec4's is vec(0,0,0,1), opaque black rather than four zeroes.
val writeXToBuffer: String A Kotlin statement to be pasted into generated vertex-writing code, with x for the value and buffer for the target. Its one consumer is writeVertexData, which nothing in the tree calls.
val sizeInBytes: Long The type's packed size: components times component size. This is the size of the value itself, not the stride a std140 block would give it.

TextureType

enum class TextureType(val dimensions: Int)

Constants: texture2D, texture2DArray.

Introspected

open class Introspected

Declaration Summary
open class Bindable(val name: String, val set: Int, val binding: Int) Anything the shader reaches through a descriptor set: a name at a set and a binding.
data class Input(val name: String, val type: Either<PrimitiveType, List<Member>>, val location: Int) One in or out variable of a stage, at a location. The type is a primitive, or the members of a struct passed between stages.
data class Member(val name: String, val offset: Int, val type: Either<PrimitiveType, List<Member>>, val arrayStride: Int = 0, var physicalPointer: Boolean = false) One field of a block, struct or push-constant range, at the byte offset the compiler gave it. A member whose type is a list of members is a nested struct, and arrayStride above zero makes it an array; physicalPointer marks a buffer-reference field, which occupies eight bytes whatever its pointed-to type says.
class UBO(name: String, val size: Int, set: Int, binding: Int, val members: List<Member>) A block of members at a set and binding, in bytes. It stands for a uniform block and for a storage buffer — ubos and ssbo are both lists of this — so the kind of binding is which list it is in, not which type it is. A storage buffer reflected from Slang has size 0, since its length is not known from the declaration.
class Texture(name: String, val type: String, set: Int, binding: Int, val arrayCount: Int = 0) : Bindable(name, set, binding) A sampled texture, with type the sampler type the shader wrote (sampler2D, sampler2DArray, …). An arrayCount above zero means an array of bindings, a descriptor array, rather than an array texture, which is part of the type instead.
class Image(name: String, val type: String, set: Int, binding: Int) : Bindable(name, set, binding) A storage image. Its type is the reflected image format, not a sampler type.
class AccelerationStructure(name: String, val type: String, set: Int, binding: Int) : Bindable(name, set, binding) A ray-tracing acceleration structure the shader traces against.
data class Sampler(val name: String, val type: String, val set: Int, val location: Int) A sampler declared on its own, apart from the texture it will be combined with. It is not a Bindable, and its location is filled with the reflected binding, so read that field as the binding number it is.
data class PushConstant(val name: String, val size: Int, val members: List<Member>) A push-constant block: its members, and the size in bytes rounded up to a multiple of 16.
data class StructureType(val name: String, val members: List<Member>, val size: Int, val bufferReference: Boolean = false) A struct the shader declares, with bufferReference true for a buffer_reference block: a struct reached through a device address rather than bound to a set.
val inputs The stage's in variables, by location.
val outputs The stage's out variables, by location.
val ubos The uniform blocks the stage declares.
val ssbo The storage buffers, held as the same UBO type the uniform blocks use.
val textures The sampled textures: a sampler2D and friends, where image and sampler arrive as one.
val images The storage images.
val accelerationStructures The acceleration structures.
val samplers The samplers declared apart from a texture. The images declared that way are not collected: the reflection reports them separately and nothing reads that, so a shader that splits image from sampler has the sampler here and the image nowhere.
val pushConstants The push-constant blocks this stage declares.
val structureTypes The structs the shader declares, named from the GLSL source, since reflection keeps the layout but not the name. Each struct is matched to its reflected members by its list of member names, and two structs with the same member names in the same order collapse to the first. A shader given as SPIR-V takes the reflection's own names instead. Empty for an interface read from Slang reflection.
val memberComments The // trailing comment a human wrote next to a member declaration in the shader source, keyed by Block.member, instance.member and (when unambiguous) the bare member — see parseGlslMemberComments for exactly how it is scraped and what it cannot see.
fun memberComment(member: String, block: String? = null): String? The comment for a member, preferring the one qualified by block (the push-constant block, struct or UBO name) and falling back to the bare member name. Null when the source said nothing.
var fixedWorkgroupSize: List<Int>? Fixed compute workgroup dimensions from SPIRV-Cross; null for other stages or specialized sizes.
lateinit var code: ShaderSPIRVUtils.SPIRV The compiled SPIR-V the reflection was read from, which a pipeline then builds its shader module out of. It is assigned only once the compile has succeeded, so reading it after a failed introspection throws rather than returning nothing.
override fun toString(): String A multi-line dump of the interface, for a log. It stops at the push constants: the declared structs, the member comments and any fixed workgroup size are not in it.

Introspection

class Introspection private constructor(val source: String, val kind: ShaderKind, val errorHandler: (String, Throwable) -> Unit = { _, throwable -> throw throwable }, val sourceName: String = kind.toString() + ".glsl", val includeResolver: ShaderIncludeResolver? = null, private val precompiledSpirv: ShaderSPIRVUtils.SPIRV? = null, private val slangReflectionJson: String? = null) : Introspected()

Declaration Summary
val source: String The GLSL that was compiled. It is also read lexically, for the struct names the reflection does not carry and for members' trailing comments, and that reading happens whether or not the compile succeeds. A shader handed over as SPIR-V has none, and falls back to the names in the reflection's own type table.
val kind: ShaderKind Which stage this is, which decides how the source is compiled and how Slang reflection is read.
val errorHandler: (String, Throwable) -> Unit Where a compile or reflection failure goes. The default rethrows, so an introspection built without one either succeeds or throws; pass a handler to get an invalid introspection back instead of an exception.
val sourceName: String The name compiler errors are reported against, VERTEX_SHADER.glsl and the like by default.
val includeResolver: ShaderIncludeResolver? Answers this source's #include lines during the compile.
var valid True once the compile and the reflection have both finished. While it is false every interface list is empty and code is unset. The member comments are there even so, since they are scraped from the source before anything is compiled.

MetalShaderErrors.kt

Explains shader failures on macOS that come from Metal name collisions rather than from the GLSL.

src/jvmMain/kotlin/afield/graphics/shader/MetalShaderErrors.kt — read the source

MetalShaderErrors

object MetalShaderErrors

What a shader failure on macOS means, when the cause is Metal rather than your GLSL.

Declaration Summary
fun explain(message: String): String? What message — Vulkan's report of a Metal compile failure — means, in plain words; null when it is not a name collision.
fun note(message: String): String? Record message if it explains a Metal failure, and return the explanation.
fun recent(withinMs: Long = 10_000): String? The explanation of a Metal failure noted in the last few seconds, for the pipeline error that follows it; null otherwise.
val reservedNames: Set<String> by lazy The names of the types Metal's standard library declares, read from the newest set of Metal headers on this machine; empty where there are none (not macOS). A function in a shader named like one of these does not build on macOS.

MslEditorLanguage.kt

The Monaco editor language for Metal Shading Language, used by the msl box property.

src/jvmMain/kotlin/afield/graphics/shader/MslEditorLanguage.kt — read the source

Top-level functions

Declaration Summary
fun mslEditorLanguageJavascript(): String The same text MslEditorLanguage.javascript returns, as a top-level function.

MslEditorLanguage

object MslEditorLanguage

The Monaco language for Field's msl box property — Metal Shading Language for a visionOS head.

Declaration Summary
const val id: String The language id, msl, as the same string languageId holds. Neither constant reaches the browser: the id Monaco registers is declared inside msl.js itself.
const val languageId: String The Monaco language id Field uses for the msl property, and for .metal / .msl files.
fun javascript(): String The text of msl.js, read from the classpath, or from src/commonMain/resources/msl.js relative to the working directory when the resource is not on it. Throws when neither is there, rather than returning an empty grammar.

ShaderAbi.kt

Decides whether a recompiled shader can be swapped in without a full reload.

graphicsShaderAbiCheck and computeShaderAbiCheck compare the old and new Introspection of each stage; a changed interface in any stage returns a ShaderAbiCheck that is not compatible, with a message asking for a full reload.

src/jvmMain/kotlin/afield/graphics/shader/ShaderAbi.kt — read the source

Top-level functions

Declaration Summary
fun graphicsShaderAbiCheck(oldVertex: Introspection, oldFragment: Introspection, newVertex: Introspection, newFragment: Introspection): ShaderAbiCheck Compares a graphics shader's vertex and fragment interfaces across a recompile.
fun computeShaderAbiCheck(oldCompute: Introspection, newCompute: Introspection): ShaderAbiCheck Compares a compute shader's interface across a recompile, on the same terms graphicsShaderAbiCheck applies to each graphics stage.

ShaderAbiCheck

data class ShaderAbiCheck(val compatible: Boolean, val message: String = "")

Declaration Summary
val compatible: Boolean True when every stage presents the same interface after the recompile as before it, so the running pipeline can take the new code instead of being rebuilt.
val message: String Empty when compatible; otherwise names the stage that changed and prints its old and new interface signatures in full. This is the text a refused Update Shader reports on the box, below the summary of what changed.

ShaderInterfaceTransition.kt

What changed in a shader's interface across a full reload, binding by binding and member by member.

shaderInterfaceTransition and graphicsShaderInterfaceTransition compare two Introspections and classify each binding as one ShaderBindingChange; the result names what was kept, added, removed, renamed, resized or retyped, so a reload can re-apply the values that still fit. Tested in ShaderInterfaceTransitionTest.

src/jvmMain/kotlin/afield/graphics/shader/ShaderInterfaceTransition.kt — read the source

Top-level functions

Declaration Summary
fun graphicsShaderInterfaceTransition(oldVertex: Introspection, oldFragment: Introspection, newVertex: Introspection, newFragment: Introspection): ShaderInterfaceTransition shaderInterfaceTransition for a graphics (vertex + fragment) module.
fun shaderInterfaceTransition(old: List<Pair<String, Introspection>>, new: List<Pair<String, Introspection>>): ShaderInterfaceTransition Compare two module interfaces, stage by stage, and classify every binding — see ShaderInterfaceTransition. Bindings are merged across a module's stages by name, as the module itself merges them (one uniform per name, one push-constant layout per member name).

ShaderBindingKind

enum class ShaderBindingKind(val label: String)

The kinds of thing a shader's interface binds, as a full reload compares them. label is how a transition summary names one: "uniform block Light grew 16→32 bytes".

Constants: UNIFORM_BLOCK, STORAGE_BUFFER, TEXTURE, IMAGE, ACCELERATION_STRUCTURE, PUSH_CONSTANT.

ShaderBindingChange

enum class ShaderBindingChange

What a full reload did to one binding (or one member of a block), old interface to new.

Constants: KEPT, ADDED, REMOVED, RENAMED, RESIZED, RETYPED.

ShaderMemberTransition

data class ShaderMemberTransition(val name: String, val change: ShaderBindingChange, val oldType: String? = null, val newType: String? = null)

A member of a uniform block, storage buffer or push-constant block, old to new, by name.

Declaration Summary
val name: String The member's name, which is what the two interfaces were matched on.
val change: ShaderBindingChange KEPT, ADDED, REMOVED or RETYPED: the four a member can be. Moving to a different offset counts as RETYPED, and members are never classified as renamed or resized.
val oldType: String? The member's type in the old interface, written the way a shader author would: vec3, float[], struct, pointer. Null when the member is new.
val newType: String? Its type in the new interface; null when it was removed.

ShaderBindingTransition

data class ShaderBindingTransition(val kind: ShaderBindingKind, val change: ShaderBindingChange, val name: String, val oldName: String = name, val oldSize: Int? = null, val newSize: Int? = null, val oldType: String? = null, val newType: String? = null, val members: List<ShaderMemberTransition> = emptyList())

One binding's fate across a reload. name is its name in the new interface (the old one for ShaderBindingChange.REMOVED); oldName differs from it only for ShaderBindingChange.RENAMED. Sizes are in bytes, for blocks. members lists the members of a kept-by-name block whose type or offset changed, or that came or went.

Declaration Summary
val kind: ShaderBindingKind Which sort of binding this is, and so how a summary line names it.
val change: ShaderBindingChange What the reload did to it.
val name: String The binding's name in the new interface. A removed binding has no new name, so this carries the old one.
val oldName: String Its name in the old interface, equal to the new name except for a rename.
val oldSize: Int? The old size in bytes, for a uniform block or storage buffer. Null for the other kinds, which have no size of their own, and for a block that is only in the new interface.
val newSize: Int? The new size in bytes, on the same terms; null for a block that is only in the old interface.
val oldType: String? The old type, for the kinds a type describes: a texture (with its array count, as sampler2D[4]), an image, an acceleration structure, or a push-constant member. Blocks say their sizes and members instead, so both type fields stay null for them.
val newType: String? The new type, on the same terms.
val members: List<ShaderMemberTransition> For a block kept under the same name, only those of its members that did not stay the same. Empty for every other case, a renamed block included — a rename is only recognised when the layout matches, so there is nothing to list.
val quiet: Boolean Kept, with no member changed.

ShaderInterfaceTransition

data class ShaderInterfaceTransition(val bindings: List<ShaderBindingTransition>, val stagesAdded: List<String> = emptyList(), val stagesRemoved: List<String> = emptyList())

Old interface to new, for one full reload of a module: every binding classified, and the stages that came or went. What shaderInterfaceTransition computes and what the reload carries buffers and values across by — see dev_docs/stdlib_behaviour_changes.md, "Full shader reload diffs the interface".

Declaration Summary
val bindings: List<ShaderBindingTransition> Every binding either interface declares, each appearing once, grouped in a fixed order: uniform blocks, storage buffers, textures, images, acceleration structures, then push-constant members. Kept bindings are in here too; changes is the filtered view.
val stagesAdded: List<String> Stages the new module has and the old one did not. A ray-tracing module's stage list varies with what it declares, and a repeated stage is numbered (closesthit#2), so adding a second hit shader arrives here.
val stagesRemoved: List<String> Stages the old module had and the new one does not.
val changes: List<ShaderBindingTransition> Everything that is not simply kept.
val quiet: Boolean Nothing changed that a person needs to hear about.
fun of(kind: ShaderBindingKind, name: String): ShaderBindingTransition? The binding of kind called name in the new interface, if it has one.
fun oldNameOf(kind: ShaderBindingKind, name: String): String The old name of the kind binding the new interface calls name: name unless it was renamed.
val staleNames: Map<String, String> The Kotlin names the box's preamble exposed for the old interface that no longer mean what they did, each with why: a removed or renamed binding, a removed or retyped block member or push constant. Code that uses one was written against the old preamble; the next run compiles against the new one.
fun summary(): String One line saying what the reload did to the interface — "uniform block Light grew 16→32 bytes, buffer reallocated; push constant tint removed" — or "" when nothing changed.

ShaderSPIRVUtils.kt

GLSL to SPIR-V compilation through shaderc, with #include resolution and line-numbered error reports.

ShaderSPIRVUtils compiles a source for one ShaderKind (vertex, fragment, compute, mesh, task and the ray-tracing stages), and throws CompilerError with the compiler's message on failure. A ShaderIncludeResolver passed in a ShaderCompileSource answers each include request.

src/jvmMain/kotlin/afield/graphics/shader/ShaderSPIRVUtils.kt — read the source

ShaderKind

enum class ShaderKind(internal val kind: Int)

Constants: VERTEX_SHADER, GEOMETRY_SHADER, FRAGMENT_SHADER, COMPUTE_SHADER, MESH_SHADER, TASK_SHADER, RAYGEN_SHADER, ANY_HIT_SHADER, CLOSEST_HIT_SHADER, MISS_SHADER, INTERSECTION_SHADER, CALLABLE_SHADER.

Declaration Summary
fun toVulkan(): Int The VK_SHADER_STAGE_* bit for this kind, as a pipeline stage wants it. Mesh and task are the VK_EXT_mesh_shader bits and the six ray-tracing kinds the VK_KHR_ray_tracing_pipeline ones, so the stage a shader compiles for is also the extension it needs.

CompilerError

class CompilerError(override val message : String) : Exception(message)

ShaderIncludeType

enum class ShaderIncludeType(internal val shadercType: Int)

Constants: RELATIVE, STANDARD.

ShaderIncludeRequest

data class ShaderIncludeRequest(val requestedPath: String, val includeType: ShaderIncludeType, val requestingSource: String?, val includeDepth: Long)

Declaration Summary
val requestedPath: String Exactly what the #include said, unresolved.
val includeType: ShaderIncludeType RELATIVE for #include "…", STANDARD for #include <…>.
val requestingSource: String? The name of the source that wrote this #include, or null when shaderc did not supply one. An unresolved include reports that case as <root>.
val includeDepth: Long How deep this include is nested, straight from shaderc. Informational: nothing here enforces a limit on it.

ShaderIncludeResolution

data class ShaderIncludeResolution(val resolvedPath: String, val source: String)

Declaration Summary
val resolvedPath: String What to call the included text. shaderc reports errors inside it against this name, and it is the requestingSource of anything the text itself includes.
val source: String The text to compile in place of the #include line.

ShaderIncludeResolver

fun interface ShaderIncludeResolver

Declaration Summary
fun resolve(request: ShaderIncludeRequest): ShaderIncludeResolution? Answers one #include, or returns null to fail the compile with "Unable to resolve include '…' from '…'". Throwing fails it the same way, with the exception's own message, so a resolver need not catch anything to produce a readable error.

ShaderCompileSource

data class ShaderCompileSource(val sourceName: String, val sourceText: String, val includeResolver: ShaderIncludeResolver? = null)

Declaration Summary
val sourceName: String What to call this source in compiler errors: a filename, or any name the reader will know.
val sourceText: String The GLSL itself. It is passed with an explicit length, so shaderc sees exactly these characters.
val includeResolver: ShaderIncludeResolver? Resolves this source's #include lines. Left null, no include callback is installed at all, so nothing can answer one.

ShaderSPIRVUtils

object ShaderSPIRVUtils

Declaration Summary
fun wrapBytecode(bytes: ByteArray): SPIRV Wraps SPIR-V that something else compiled (slangc's output, or a .spv read from disk) as a SPIRV the pipeline code accepts. The bytes are copied into a direct, native-order buffer with no shaderc result behind it, so freeing the result releases only the reference.
fun compileShaderFile(shaderFile: String?, shaderKind: ShaderKind): SPIRV? Compiles a shader stored as a classpath resource, named the way the system class loader wants it. A name no resource matches throws rather than returning null — the null result is for a resource that is found and then cannot be read.
fun compileShaderAbsoluteFile(shaderFile: String, shaderKind: ShaderKind): SPIRV? Compiles the shader at a file: URI, as compileShaderFile hands one over. Returns null when the URI will not parse or the file will not read, printing the stack trace; a compile error still arrives as CompilerError.
fun compileShader(filename: String, source: String?, shaderKind: ShaderKind): SPIRV Compiles source under the name filename, or reads the file at filename when source is null. No include resolver, so this overload is for a self-contained stage.
fun compileShader(source: ShaderCompileSource, shaderKind: ShaderKind): SPIRV The compile every other overload ends at: shaderc, targeting Vulkan 1.3 and SPIR-V 1.5, with main as the entry point and this source's resolver answering its includes. Debug info is emitted only when validation or shader printf is on, and the whole call is timed as shader.compile.
class SPIRV(private val handle: Long, private var bytecode: ByteBuffer?) : NativeResource A compiled module: the bytes a VkShaderModule is created from. When compileShader produced it those bytes are shaderc's own, so freeing it releases shaderc's result and the buffer stops being readable; when wrapBytecode did, there is no result and freeing only drops the reference.

ShadertoyChannels.kt

What a ShaderToy Image pass reads and draws onto in Field: ShadertoyChannel (a texture turned the way ShaderToy samples one), ShadertoyCanvas (the picture a document's shadertoy boxes share, shown in the renderer window) and webcam (the camera as a channel).

shadertoyHere builds the module; these give it inputs and somewhere to draw. The shadertrinket starting point uses all three:

val canvas = shadertoyCanvas()
canvas += shadertoyHere {
    +canvas.rtt
    textures("iChannel0" to webcam())
}

src/jvmMain/kotlin/afield/graphics/shader/ShadertoyChannels.kt — read the source

Top-level functions

Declaration Summary
fun shadertoyCanvas(width: Int = 960, height: Int = 540, name: String = "shadertrinket", inWindow: Boolean = true, renderer: Renderer = fromContext()): ShadertoyCanvas This renderer's shadertoy canvas called name, made width×height on first use and shared after that (asking again with another size keeps the first, and says so). See ShadertoyCanvas.
fun Box.webcam(device: Int = 0, width: Int = 1280, height: Int = 720, renderer: Renderer = fromContext()): HasImageView The camera device as a texture, for textures("iChannel0" to webcam()): opened once per renderer at width×height (if the camera offers it) and kept open across re-runs, and uploaded every frame as a renderer pre-target.

ShadertoyChannel

class ShadertoyChannel private constructor(val renderer: Renderer, source: Pair<HasImageViewArray, TextureSampler>, val width: Int, val height: Int, sourceFormat: Int)

A texture as a ShaderToy iChannel sees it: a copy, redrawn every frame, that is bottom-up and holds display values.

Declaration Summary
val renderer: Renderer The renderer this channel's flip pass runs on, and the first half of the key it is cached under: one channel per renderer and per source view.
val width: Int The source's width in pixels (and so this channel's iChannelResolution).
val height: Int The source's height in pixels.
val target The flipped copy, which the shadertoy module samples.
val sampler: TextureSampler Linear filtering, clamped at the edges.

ShadertoyCanvas

class ShadertoyCanvas internal constructor(val renderer: Renderer, val name: String, val width: Int, val height: Int, inWindow: Boolean)

The picture a document's shadertoy boxes draw into, like ShaderTrinket's canvas: a width×height render target, shown letterboxed in the renderer window, that can also be published or bound as another shader's iChannel (it is rtt).

Declaration Summary
val renderer: Renderer The renderer that holds the canvas and composites it. Its window supplies the aspect the canvas is letterboxed into and the cursor that becomes iMouse.
val name: String What this canvas is called, shadertrinket by default. Canvases are shared per renderer and per name, so two boxes asking for the same name draw into the same picture; it also names the render target and the compositing module in the renderer's lists.
val width: Int The canvas's width in pixels, and the x of iResolution for every module drawn into it. Fixed at first use: asking for the same name at another size keeps this one and logs a warning.
val height: Int The canvas's height in pixels, fixed at first use in the same way as the width.
val rtt The canvas itself: _SRGB, so the display values a ShaderToy program writes are what it stores.
fun fitInWindow(): Vector4f The canvas's rectangle in the window, as fractions (x, y, w, h) from the top left.
val surface iResolution and iMouse for modules drawn into this canvas.
var inWindow: Boolean Whether the window shows this canvas. Turn it off for a canvas that is only another shader's channel.
operator fun plusAssign(module: ModulePointer) Draw module (a shadertoyHere module built with +canvas.rtt) as this canvas's picture.

ShadertoyPublish.kt

ShaderTrinket's Publish, as a function: publishShadertoyCanvas streams a shader canvas over WebRTC under a name, so what a viewer sees is whatever that canvas is showing, presets included.

src/jvmMain/kotlin/afield/graphics/shader/ShadertoyPublish.kt — read the source

Top-level functions

Declaration Summary
fun Box.publishShadertoyCanvas(name: String = "field-shader", canvas: ShadertoyCanvas = shadertoyCanvas(), network: WebRTCNetwork = WebRTCNetwork.default): WebRTCSink ShaderTrinket's Publish: stream canvas to the class as login/name, with the stream's preview and status in this box's output.

SlangCompiler.kt

Compiles Slang shaders by running an external slangc, to GLSL, SPIR-V and reflection JSON.

SlangCompiler looks for the executable in AFIELD_SLANGC, then -Dafield.slangc, then the pinned download, the Vulkan SDK and PATH. compileVertexFragment and compileSharedStage return one SlangStageArtifacts per stage, ready for Introspection.

src/jvmMain/kotlin/afield/graphics/shader/SlangCompiler.kt — read the source

SlangEntryPoint

data class SlangEntryPoint(val name: String, val stage: String)

Declaration Summary
val name: String The function's name, as the reflection JSON spells it. It is what reaches slangc as -entry on the compile that follows.
val stage: String The stage the function is written for, lowercased: vertex, fragment, compute, mesh, task, or one of the ray-tracing stages such as closesthit. Compiling a stage matches this against the slangc stage word for the shader kind asked for.

SlangStageArtifacts

data class SlangStageArtifacts(val kind: ShaderKind, val entryPoint: SlangEntryPoint, val glslSource: String, val reflectionJson: String, val spirv: ShaderSPIRVUtils.SPIRV)

Declaration Summary
val kind: ShaderKind The shader kind this stage was compiled as, which chose the profile and the stage word.
val entryPoint: SlangEntryPoint The one entry point in the source that claimed this stage. A file with none, or with more than one, does not compile.
val glslSource: String The GLSL slangc emitted for this stage, with parameter names and layout preserved (-no-mangle, -preserve-params, column-major matrices) so that introspecting it recovers the names the Slang source used.
val reflectionJson: String slangc's reflection JSON, as emitted beside the SPIR-V rather than beside the GLSL, so it describes the SPIR-V binding numbers.
val spirv: ShaderSPIRVUtils.SPIRV The compiled SPIR-V, already in the off-heap buffer the Vulkan calls want.

SlangCompiler

object SlangCompiler

Declaration Summary
fun compileToGlsl(source: String, kind: ShaderKind, entryFileName: String): String Translates source to GLSL for one stage and returns the text, with no entry point named and no SPIR-V or reflection asked for. For a stage that will be introspected and loaded, use compileSharedStage instead.
fun compileVertexFragment(source: String, entryFileName: String = "vertex_fragment.slang"): Pair<SlangStageArtifacts, SlangStageArtifacts> Compiles both halves of a graphics shader out of one Slang file, which is where Slang differs from Field's GLSL pair: one source holds a vertex entry point and a fragment one, and the structs they pass between them are written once. The file is compiled twice, once per stage.
fun compileSharedStage(source: String, kind: ShaderKind, entryFileName: String): SlangStageArtifacts Compiles one stage of a Slang source and returns everything Field needs to load it. Three slangc runs: one with -no-codegen to list the file's entry points, then one for GLSL and one for SPIR-V and reflection. The source must declare exactly one entry point for this stage. None, or two, is a CompilerError naming the ones it found.

GpuShaderLearningView.kt

An editable render-to-texture teacher next to a learner that trains on the GPU while it runs.

GpuShaderLearningView pairs a source RenderToTexture with GpuShaderTraining; with learning off and the learned-only view selected, the source stops running and the learned function stays.

src/jvmMain/kotlin/afield/graphics/shader/learning/GpuShaderLearningView.kt — read the source

GpuShaderLearningView

class GpuShaderLearningView(val renderer: Renderer, val source: RenderToTexture, val name: String = "gpu-learning", private val setSourceParameter: (Float) -> Unit) : Target, AutoCloseable

An editable RTT teacher and its live GPU learner. Source lifetime remains with the caller. View 1 with learning=false stops executing the source entirely; its learned function remains. parameter is normalized to [-1,1], with the same value supplied to the teacher and network.

Declaration Summary
val renderer: Renderer
val source: RenderToTexture
val name: String
val model
var learning
var iterationsPerFrame
var parameter
var view
var sourceRenderCount
val output
fun install(): GpuShaderLearningView
override fun render(command: CommandBuffer, index: Int)
override fun close()

GpuShaderTraining.kt

On-GPU training of the learned RGB function, with no CPU image or weight readback.

GpuShaderTraining is the in-renderer alternative to the offline ShaderTraining path. It needs a device with cooperative-vector training enabled and the prebuilt training shaders under build/lighting/learned-shader/; otherwise its constructor refuses and the offline path remains.

src/jvmMain/kotlin/afield/graphics/shader/learning/GpuShaderTraining.kt — read the source

GpuShaderTraining

class GpuShaderTraining(val renderer: Renderer,val width: Int,val height: Int,val batchSize: Int=256,val seed: Int=42):AutoCloseable

A small reusable learned RGB function, trained directly from Field images on the graphics queue. Architecture matches LearnedShader: 16 positional features -> 32 ReLU -> 32 ReLU -> RGB. Slang differentiates the network; cooperative-vector outer products accumulate its gradients. FP32 Adam master weights/moments, loss-scaled FP16 gradients and inference matrices. The source remains caller-owned. Train/evaluate/close only between or within render-thread work. No CPU image/weight readback is used in train/evaluate. This is not a general graph ML runtime.

Declaration Summary
data class Layer(val inputs: Int,val outputs: Int,val matrixOffset: Int,val biasOffset: Int,val matrixBytes: Int)
val layers: List<Layer>
val trainingBytes: Int
val output
var steps
var learningRate
fun reset() Restart the deterministic initial model, including optimizer state, on the next recorded call.
fun train(command: CommandBuffer,image: HasImageView,parameter: Float,iterations: Int=1) Sample the already-rendered source at this normalized parameter, then perform iterations Adam updates. Each step uses a new reproducible minibatch. Every eighth hashed pixel is held out; evaluate that subset separately when judging generalization.
fun evaluate(command: CommandBuffer,index: Int,parameter: Float) Evaluate the learned function at any point in the trained parameter interval; source is not rendered.
fun save(directory: File,sourceShaderDigest: String,fixedState: String): File Blocking, explicit checkpoint BETWEEN frames. Converts opaque training matrices to the existing portable LearnedShader row-major format. No training-optimal layout escapes into the file. The caller records the source digest and fixed-state description it actually used. A new directory prevents a partial checkpoint from replacing a working model.
override fun close()

GpuShaderTrainingSmoke.kt

src/jvmMain/kotlin/afield/graphics/shader/learning/GpuShaderTrainingSmoke.kt — read the source

GpuShaderTrainingSmoke

object GpuShaderTrainingSmoke

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

LearnedShader.kt

Runs a trained shader function as a compute pass that writes its own render target.

LearnedShader loads a model directory written by training, evaluates it each frame for the current parameter, and can LearnedShader.attach itself to the renderer's pre-target list. It uses cooperative vectors when the device supports them and a scalar variant otherwise.

src/jvmMain/kotlin/afield/graphics/shader/learning/LearnedShader.kt — read the source

LearnedShader

class LearnedShader(val renderer: Renderer, val width: Int = 1920, val height: Int = 1080, val cooperative: Boolean = renderer.device.supportsCooperativeVector, weightsFile: File = File("public/docs/assets/learned_shader/weights.f16"), manifestFile: File = File("public/docs/assets/learned_shader/weights.json"), val name: String = "learned-shader", attach: Boolean = true) : Target, AutoCloseable

A frozen, user-trained shader function. Weights are owned by this view; replace the view at a frame boundary to change weights. The ordinary arithmetic variant needs no cooperative vectors. Architecture/layout are explicit and checked, not a general ML graph runtime.

Declaration Summary
val renderer: Renderer
val width: Int
val height: Int
val cooperative: Boolean
val name: String
val gpuMilliseconds
val parameterRange: ClosedFloatingPointRange<Float>
var time
fun setParameter(value: Float) Map the original capture parameter (for example seconds) into the network's normalized time.
var view 0 original function, 1 learned function, 2 original / learned / absolute error x8.
val output
val allocatedBytes
fun attach(): LearnedShader
override fun render(command: CommandBuffer, index: Int)
override fun close()

LearnedShaderSmoke.kt

src/jvmMain/kotlin/afield/graphics/shader/learning/LearnedShaderSmoke.kt — read the source

LearnedShaderSmoke

object LearnedShaderSmoke

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

ShaderLearningChecks.kt

Acceptance checks for a learned shader, run from a box beneath the moduleHere starting point once training finishes.

ShaderLearningChecks.run measures a ShaderLearningView at a held-out parameter: full-image error, timing, and whether a rejected model leaves the previous one attached. ShaderLearningChecks.stripeMetrics measures error at the bundled test pattern's hard stripe edge separately, because a whole-image mean can hide edge smoothing.

src/jvmMain/kotlin/afield/graphics/shader/learning/ShaderLearningChecks.kt — read the source

ShaderLearningChecks

object ShaderLearningChecks

Called from a child of the actual moduleHere Starting Point, after its training process completes.

Declaration Summary
fun stripeMetrics(original: FloatArray,learned: FloatArray,width: Int,height: Int): JSONObject Measure the bundled hard stripe separately: a whole-image mean can hide edge smoothing.
fun run(lab: ShaderLearningView,directory: File=File("build/neural-tools/from-module"))

ShaderLearningView.kt

Side-by-side comparison of a real module's render target with its learned replacement.

ShaderLearningView can show the learned output alone, skipping the source pass, or source, learned output and their amplified RGB error together. Load a model with it between frames.

src/jvmMain/kotlin/afield/graphics/shader/learning/ShaderLearningView.kt — read the source

ShaderLearningView

class ShaderLearningView(val renderer: Renderer, val source: RenderToTexture, val name: String = "shader-learning", private val setSourceParameter: (Float) -> Unit) : Target, AutoCloseable

Compare a real module's render target with its learned replacement. View 1 skips the source pass completely; view 2 renders source / learned / absolute RGB error x8. Source resources stay caller-owned. Construct, load and close between frames. A rejected model leaves the last working model attached.

Declaration Summary
val renderer: Renderer
val source: RenderToTexture
val name: String
var parameter
var view
var model: LearnedShader?
val viewGpuMilliseconds
var sourceRenderCount
val sourceGpuMilliseconds
val output
fun load(directory: File, cooperative: Boolean = renderer.device.supportsCooperativeVector)
override fun render(command: CommandBuffer, index: Int)
override fun close()

ShaderTraining.kt

Offline distillation of a Field render target into a small learned shader: capture samples, then train them in Python.

ShaderTraining.capture renders the source RenderToTexture across a parameter range and writes train and held-out sample files, keyed by a digest of the source's shaders. ShaderTraining.trainAsync runs tools/lighting/train_shader.py through uv as a child process and returns a ShaderTrainingJob whose result completes with the model directory. The source shaders keep running in Vulkan; only their output images reach Python.

src/jvmMain/kotlin/afield/graphics/shader/learning/ShaderTraining.kt — read the source

ShaderTraining

object ShaderTraining

Offline image-space distillation of a real Field render target: normalized (x,y,parameter) -> linear RGB. Source shaders and their existing bindings execute in Vulkan; they are never translated to Python. Geometry, camera, textures and other uniforms must stay fixed unless represented by capture's parameter.

Declaration Summary
fun sourceDigest(source: RenderToTexture): String
fun capture(renderer: Renderer,source: RenderToTexture,directory: File, parameterRange: ClosedFloatingPointRange<Float> = -1f..1f,trainingFrames: Int=48, validationFrames: Int=13,pixelsPerFrame: Int=2048,seed: Long=4090, fixedState: String="Fixed camera, geometry, textures and all other uniforms", setParameter: (Float)->Unit): File Blocking export, called BETWEEN frames. Writes a new directory only; failures leave no valid manifest. Rows are six little-endian float32 values: normalized x,y,t followed by unclamped linear r,g,b. Validation uses different parameter slices, not merely different pixels from the training images.
fun trainAsync(dataset: File,modelDirectory: File,steps: Int=12000): ShaderTrainingJob Train in a separate CPU process. This never evaluates a shader or mutates GPU state on that worker.

ShaderTrainingJob

class ShaderTrainingJob internal constructor(private val process: Process,val modelDirectory: File,val log: File): AutoCloseable

Poll result from a Field frame; close cancels the child process, including on scene Stop.

Declaration Summary
val result: CompletableFuture<File>
override fun close()

shaders.kt

The bridge between a box's text and a live Vulkan pipeline: shader source lives in box properties, and this file turns it into modules and keeps them hot-reloading.

The properties come first — vertex, fragment, compute, task/mesh, the ray-tracing stages, and a _s variant of each for Slang — alongside once.vertex { }-style seeders. The user-facing entry points are the macro placeholders Box.moduleHere, Box.computeModuleHere, Box.meshModuleHere, Box.rayTracingModuleHere and Box.shadertoyHere; each one throws MacroNotExpandedException if actually called, because MacroRegistry is expected to rewrite it into the real _moduleHere-family call.

Before compilation every stage goes through preprocess, which resolves {% property %} template tags against other boxes' shader properties (with cycle detection and #line fixups) and against built-in includes such as the field probe, field stats and quantile warp. Registered include roots — registerShaderFilesystemIncludeRoot for a directory, registerLygiaIncludeRoot / shaderCachedUrlIncludeRootResolver for URL-fetched, disk-cached libraries — are not reachable from here: they are resolved by shaderc during compilation, from a real #include only. It also expands {% copyPC %} (copying a previous stage's push-constant block) and stackAuto template slots compiled from StackSL via stackAutoShaderSource.

reloadShader, reloadComputeShader, reloadMeshShader and reloadRayTracingShader are the reload path: compile, report errors back onto the box's source lines, introspect the result, and rewrite the box's Kotlin preamble with generated binding structures before swapping the module. Box.attachShader adopts an externally built module into that same path. Box.fieldProbeHere, Box.fieldStatsHere and Box.fieldProbeAutoControl attach the GPU readback instrumentation.

Everything here needs a live Box — properties, error reporting, preamble edits and the generation-guarded async reload all key off it.

src/jvmMain/kotlin/afield/graphics/shader/shaders.kt — read the source

Used in: Field 4 Graphics, A learned shader function, Editing a document from outside Field

Top-level functions

Declaration Summary
fun stackAutoShaderReport(source: String, name: String = "program", entrypoint: String? = null, sampleTexture: String = "source"): StackAutoReport Compile one StackSL program the way a stack.auto slot compiles it, and report what that turned up beyond success or failure.
fun stackAutoShaderSource(module: ModulePointer, name: String): String The StackSL source currently bound to the {{ stack.auto.name }} slot of this graphics module's shaders — the getter behind the generated module.name property.
fun stackAutoShaderSource(module: ComputeModulePointer, name: String): String The StackSL source bound to a compute module's stack.auto slot; "" when unbound. See the ModulePointer overload for the ownership and empty-string rules.
fun stackAutoShaderSource(module: MeshModulePointer, name: String): String The StackSL source bound to a mesh module's stack.auto slot; "" when unbound. See the ModulePointer overload for the ownership and empty-string rules.
fun stackAutoShaderSource(module: ModulePointer, name: String, source: String) Bind new StackSL source to a graphics module's {{ stack.auto.name }} slot and queue the pipeline update — the setter behind the generated module.name = "…" property, and the fastest live-coding loop in the file.
fun stackAutoShaderSource(module: ComputeModulePointer, name: String, source: String) Bind StackSL source to a compute module's stack.auto slot; queues the same coalesced, ABI-preserving background update as the graphics stackAutoShaderSource setter.
fun stackAutoShaderSource(module: MeshModulePointer, name: String, source: String) Bind StackSL source to a mesh module's stack.auto slot.
fun registerShaderIncludeRoot(root: String, resolver: ShaderIncludeRootResolver) Claim a first path segment for a resolver, so #include "root/some/file.glsl" in any box's shader is answered by resolver instead of being looked for as a box name.
fun unregisterShaderIncludeRoot(root: String): ShaderIncludeRootResolver? Remove an include root, returning the resolver that was registered under it (or null if none was). Shaders already compiled keep working — this only affects the next compile.
fun listShaderIncludeRoots(): Set<String> Every include-root name currently registered, sorted — the quickest way to answer "why isn't #include "lygia/…" resolving?" from a box.
fun shaderCachedUrlIncludeRootResolver(root: String, baseUrl: String, cacheOptions: ShaderRemoteIncludeCacheOptions = ShaderRemoteIncludeCacheOptions(cacheDirectory = defaultShaderIncludeCacheDirectory().resolve(normalizeShaderIncludeRootName(root)))): ShaderIncludeRootResolver An include root that fetches baseUrl/<path> over HTTP the first time and reads the local copy every time after — how Field consumes a shader library that lives on a website rather than on disk.
fun shaderLygiaIncludeRootResolver(cacheOptions: ShaderRemoteIncludeCacheOptions = ShaderRemoteIncludeCacheOptions(cacheDirectory = defaultShaderIncludeCacheDirectory().resolve("lygia.xyz"))): ShaderIncludeRootResolver The Lygia GLSL library as an include root, cached under ~/.field4/shader-include-cache/lygia.xyz by default — one call and #include "lygia/math/const.glsl" works.
fun shaderLygiaIncludeRootResolver(cacheDirectory: Path): ShaderIncludeRootResolver The Lygia include root with the cache pointed somewhere of your choosing and the default timeouts — a per-project cache directory, say, rather than the shared one under ~/.field4.
fun shaderLygiaIncludeRootResolver(cacheDirectory: String): ShaderIncludeRootResolver The Lygia include root with the cache directory given as a string. Interpreted by Paths.get, so a relative path is relative to the JVM's working directory — not to the workspace. Prefer an absolute path.
fun registerLygiaIncludeRoot(cacheOptions: ShaderRemoteIncludeCacheOptions = ShaderRemoteIncludeCacheOptions(cacheDirectory = defaultShaderIncludeCacheDirectory().resolve("lygia.xyz"))) Register Lygia under the include root lygia, so every box's shaders can #include "lygia/…".
fun registerLygiaIncludeRoot(cacheDirectory: Path) Register Lygia under the root lygia, caching into cacheDirectory.
fun registerLygiaIncludeRoot(cacheDirectory: String) Register Lygia under the root lygia, caching into a directory named by string — Paths.get-relative to the JVM's working directory, so prefer an absolute path.
fun shaderFilesystemIncludeRootResolver(directory: Path): ShaderIncludeRootResolver An include root served straight from a directory on disk: #include "root/sub/thing.glsl" reads directory/sub/thing.glsl.
fun shaderFilesystemIncludeRootResolver(directory: String): ShaderIncludeRootResolver A directory-backed include root named by string. Paths.get-relative to the JVM's working directory, which is not the workspace directory — pass an absolute path unless you are certain.
fun registerShaderFilesystemIncludeRoot(root: String, directory: Path) Register directory as the include root root, so #include "root/…" reads files under it — the one-liner for pulling a checked-out shader library into a workspace.
fun registerShaderFilesystemIncludeRoot(root: String, directory: String) Register a directory as an include root, with the directory given as a string — Paths.get-relative to the JVM's working directory, so prefer an absolute path.
fun Box.fieldProbeHere(samples: Int = afield.graphics.vulkan.util.FIELD_PROBE_DEFAULT_SAMPLES, binding: Int = afield.graphics.vulkan.util.FIELD_PROBE_DEFAULT_BINDING, descriptorSet: Int = afield.graphics.vulkan.util.FIELD_PROBE_DEFAULT_SET, enabled: Boolean = true, ordinalRange: UIntRange? = null): ShaderFieldProbe Create (or reconfigure) this box's GPU sample probe and hand it back for chaining — the Kotlin half of #include "fieldProbe", which lets a shader call fieldProbe(x) and see the resulting distribution on the CPU.
fun Box.fieldStatsHere(channels: Int = 1, sketchSize: Int = 256, maxPartials: Int = 65_536, descriptorSet: Int = 0, partialBinding: Int = FIELD_STATS_DEFAULT_PARTIAL_BINDING, resultBinding: Int = FIELD_STATS_DEFAULT_RESULT_BINDING, stateSlots: Int = 8): ShaderFieldStats Declare that this box produces a GPU statistics block, and get the handle to read it back with — the Kotlin half of #include "fieldStats" in a shader that records into it.
fun Box._fieldStatsHere(channels: Int = 1, sketchSize: Int = 256, maxPartials: Int = 65_536, descriptorSet: Int = 0, partialBinding: Int = FIELD_STATS_DEFAULT_PARTIAL_BINDING, resultBinding: Int = FIELD_STATS_DEFAULT_RESULT_BINDING, stateSlots: Int = 8): ShaderFieldStats The real work behind Box.fieldStatsHere — what the macro rewrites the call site to. Reach for it directly only when you are driving a box from outside and do not want macro expansion.
fun Box.macro_fieldStatsHere(args: List<String>): Insert Macro expansion for fieldStatsHere(...): parse the argument text, allocate the stats block now (at expansion time, so a computeModuleHere in the same fragment can already resolve #include "fieldStats"), and rewrite the call to Box._fieldStatsHere.
fun Box.useFieldStats(stats: ShaderFieldStats): ShaderFieldStats Bind another box's ShaderFieldStats into this box as a consumer, so #include "fieldStats" here emits the read-only GLSL instead of the recording GLSL.
fun Box.fieldProbeAutoControl(probe: ShaderFieldProbe = ensureFieldProbe(), controller: AdaptiveDistributionController = AdaptiveDistributionController(ControllerConfig(), UniformQuantileFeatureExtractor()), config: ShaderProbeAutoControlConfig = ShaderProbeAutoControlConfig(), install: Boolean = true, runloop: Runloop = Runloop.runLoop, apply: (params: DoubleArray, output: ControlOutput, batch: FieldProbeScalarBatch) -> Unit): ShaderFieldAutoController Wire this box's probe into an adaptive controller, so a shader parameter is steered every frame by what the shader's own output distribution turns out to be.
fun Box.moduleHere(p: Parameters): ModulePointer Macro placeholder expanded to a graphics ModulePointer for the current box.
fun Box.moduleHere(p: Parameters.() -> Unit): ModulePointer Macro placeholder for the trailing-lambda form, moduleHere { primitiveType = triangles }.
fun Box.computeModuleHere(p: Parameters): ComputeModulePointer Macro placeholder expanded to a ComputeModulePointer built from this box's compute property.
fun Box.computeModuleHere(p: Parameters.() -> Unit): ComputeModulePointer Macro placeholder for computeModuleHere { … }, the trailing-lambda form. The lambda configures Parameters, not the shader source. Calling it directly throws.
fun Box.meshModuleHere(p: Parameters): MeshModulePointer Macro placeholder expanded to a MeshModulePointer built from this box's task, mesh and fragment properties — all three are required, and defaults are seeded for any that are missing.
fun Box.meshModuleHere(p: Parameters.() -> Unit): MeshModulePointer Macro placeholder for meshModuleHere { … }, the trailing-lambda form. Calling it directly throws.
fun Box.rayTracingModuleHere(p: Parameters): RayTracingModulePointer Macro placeholder expanded to a RayTracingModulePointer built from this box's raygen property plus whichever of miss/closesthit/anyhit/intersection/callable are non-blank.
fun Box.rayTracingModuleHere(p: Parameters.() -> Unit): RayTracingModulePointer Macro placeholder for rayTracingModuleHere { … }, the trailing-lambda form. Calling it directly throws.
fun Box.shadertoyHere(p: Parameters): ModulePointer Macro placeholder expanded to a Shadertoy-shaped graphics module: a full-screen triangle, the iResolution/iTime/iMouse/iDate uniform block already bound, four 1×1 dummy iChannel textures, and depth testing off — so pasting a mainImage body into the box's fragment shader mostly just works.
fun Box.shadertoyHere(p: Parameters.() -> Unit): ModulePointer Macro placeholder for shadertoyHere { … }, the trailing-lambda form. Calling it directly throws.
fun numberFromErrorMessage(m: String): Int? Dig a line number out of a shader (or Vulkan) error message, so the error can be pinned to a line in the box's editor rather than dumped in the log.
fun preprocess(inside: Box, code: String, kind: ShaderKind): String Run one shader stage's source through Field's preprocessor and hand back the GLSL that shaderc would see — the thing to call when you are building a module by hand instead of through a moduleHere-family macro.
fun normalizePreambleText(preamble: String): String Tidy a box's preamble text: drop duplicate declarations, hoist every import to the top, and remove blank lines.
fun applyInsertToPreamble(box: Box, e: Insert) Fold an Insert's generated declarations into a box's preamble — how a shader reload gets its new typed bindings in front of the Kotlin compiler.
fun reloadShader(box: Box): Boolean Recompile this box's vertex and fragment stages from its shader properties and swap the live graphics module — the Reload Shader action (alt-r), as a function.
fun Box.attachShader(module: ModulePointer, overwriteProperties: Boolean = false): ModulePointer Adopt a graphics module you built yourself into this box's hot-reload machinery: the box gains the Reload Shader and Update Shader actions, its shader properties are seeded from the module's source, and the module becomes box._shader.
fun reloadComputeShader(box: Box): Boolean Recompile this box's compute stage and swap the live compute module — the Reload Compute Shader action (alt-r) as a function.
fun Box.attachShader(module: ComputeModulePointer, overwriteProperties: Boolean = false): ComputeModulePointer Adopt an externally built compute module into this box: install the reload and update actions, seed the compute property from the module's source, take it as box._computeShader, and reload once.
fun reloadMeshShader(box: Box): Boolean Recompile all three stages of this box's mesh-shader pipeline — task, then mesh, then fragment — and swap the live module.
fun Box.attachShader(module: MeshModulePointer, overwriteProperties: Boolean = false): MeshModulePointer Adopt an externally built mesh-shader module into this box, seeding the task, mesh and fragment properties and reloading once.
fun reloadRayTracingShader(box: Box): Boolean Recompile whichever ray-tracing stages this box has and swap the live module.
fun Box.attachShader(module: RayTracingModulePointer, overwriteProperties: Boolean = false): RayTracingModulePointer Adopt an externally built ray-tracing module into this box, seeding raygen (required) and whichever optional stages the module actually has.
fun Box.macro_moduleHere(args: List<String>): Insert? Macro expansion for moduleHere(…) — the entry point MacroRegistry calls when it finds one in a box's text. Delegates to Box.macro_moduleHereWithDefaultShaders with Field's stock vertex and fragment shaders as the fallbacks.
fun Box.macro_moduleHereWithDefaultShaders(args: List<String>, vertex: String = defaultVertexShader, fragment: String = defaultFragmentShader, resolvesToCall: String = "_moduleHere"): Insert? The shared body of every graphics-module macro: seed the box's shader properties if it has none, preprocess and compile both stages at expansion time, and return the Insert that carries the generated typed bindings plus the rewritten call site.
fun Box.macro_shadertoyHere(args: List<String>): Insert? Macro expansion for shadertoyHere(…): register the shadertoy template preambles on the box, then run the ordinary graphics expansion with the shadertoy vertex/fragment shaders as fallbacks and _shadertoyHere as the runtime call.
fun insertForShader(vi: Introspection, fi: Introspection, resolvesToCall: String = "_moduleHere", args: List<String>, box: Box? = null): Insert Turn a compiled vertex+fragment pair into the Insert that gives a box its typed graphics API: one val Mesh.<name> by FromInput() per vertex attribute, the m.set { } writer map, a generated Kotlin class per GLSL struct/UBO/SSBO layout, stack.auto string properties, and Mesh/MeshGroup accessors for every push-constant member. UBO members get less than that: the block itself becomes a val ModulePointer.<name> by FromUniform() and each member a var Structure.<name>, with no Mesh/MeshGroup accessor, so a UBO member is written through the uniform rather than per-mesh. (The compute and mesh-shader inserts do emit Mesh.<uboName>; this graphics path does not.)
fun Box._moduleHere(p: Parameters.() -> Unit): ModulePointer The runtime graphics-module builder for the trailing-lambda call site, moduleHere { … } after expansion. Resolves the Renderer from the ambient context — which means this box must be a descendant of the box that owns the window, or fromContext() finds nothing.
fun Box._moduleHere(p: Parameters = parameters {}): ModulePointer Build this box's graphics module and store it as box._shader — the function every expanded moduleHere call actually ends up in.
fun Box._shadertoyHere(p: Parameters.() -> Unit): ModulePointer The runtime shadertoy-module builder for the trailing-lambda call site. Resolves the Renderer from the ambient context, so the box must be a descendant of its renderer box.
fun Box._shadertoyHere(p: Parameters = parameters {}): ModulePointer Build a shadertoy-shaped module: an ordinary Box._moduleHere with depth testing off, the shadertoy uniform block bound to live values, the four iChannels bound, and a 3-vertex mesh named "shadertoy" already attached so the box draws without you building any geometry.
fun fromViews(views: List<HasImageView>, name: String = "", sampler: TextureSampler? = null): Pair<HasImageViewArray, TextureSampler> Package a fixed list of image views as the (provider, sampler) pair a texture resolver returns — the usual way to answer "bind these images to that sampler".
fun fromViews(provider: HasImageViewArray, name: String = "", sampler: TextureSampler? = null): Pair<HasImageViewArray, TextureSampler> Package a live view provider as the (provider, sampler) pair a texture resolver returns — the overload to prefer when the thing you are binding can change its views underneath you.
fun Box.macro_computeModuleHere(args: List<String>): Insert Macro expansion for computeModuleHere(…): install the reload and update actions, seed a trivial outData[i](workspace.md#stdlib-kt--i) = inData[i](workspace.md#stdlib-kt--i) + 1 kernel if the box has no compute source, compile it, and return the Insert carrying the generated ComputeMesh bindings and the rewritten _computeModuleHere call.
fun Box.macro_computeModuleHereWithDefaultShader(args: List<String>, compute: String? = null, resolvesToCall: String = "_computeModuleHere"): Insert Seed a specialised compute shader while retaining normal Field reflection, linking and reload actions.
fun Box._computeModuleHere(p: Parameters.() -> Unit): ComputeModulePointer The runtime compute-module builder for the trailing-lambda call site, computeModuleHere { … } after expansion. Resolves the Renderer from the ambient context, so the box must be a descendant of its renderer box.
fun Box._computeModuleHere(p: Parameters): ComputeModulePointer Build this box's compute module and store it as box._computeShader.
fun Box.macro_meshModuleHere(args: List<String>): Insert Macro expansion for meshModuleHere(…): install the reload action, seed a runnable task/mesh/fragment trio for any of the three the box is missing, compile all three in order, and return the Insert with the generated MeshMesh bindings.
fun Box._meshModuleHere(p: Parameters.() -> Unit): MeshModulePointer The runtime mesh-module builder for the trailing-lambda call site. Resolves the Renderer from the ambient context, so the box must be a descendant of its renderer box.
fun Box._meshModuleHere(p: Parameters = parameters {}): MeshModulePointer Build this box's mesh-shader module from the prepared _task, _mesh and _fragment properties and store it as box._meshShader.
fun Box.macro_rayTracingModuleHere(args: List<String>): Insert Macro expansion for rayTracingModuleHere(…): install the reload action, seed defaultRaygenShader if the box has no raygen, compile every stage the box does have, and return the Insert with the generated RayTracingDispatch bindings.
fun Box._rayTracingModuleHere(p: Parameters.() -> Unit): RayTracingModulePointer The runtime ray-tracing-module builder for the trailing-lambda call site. Resolves the Renderer from the ambient context, so the box must be a descendant of its renderer box.
fun Box._rayTracingModuleHere(p: Parameters = parameters {}): RayTracingModulePointer Build this box's ray-tracing module from the prepared stage properties and store it as box._rayTracingShader.

Top-level values

Declaration Summary
var Box.fragment: String? The fragment-stage GLSL for this box, as text — the shader tab you type into, and the only place a graphics module's fragment shader comes from (assignments inside a moduleHere { } lambda do not set it).
var Box.vertex: String? The vertex-stage GLSL for this box. Its layout(location = …) in declarations are the contract that generates the box's Mesh writers (pos, col, …), so changing an input here changes the Kotlin API of every box downstream of the next reload.
var Box.vertex_fragment: String? One Slang source holding both the vertex and fragment stages, compiled twice (once per stage) by slangc. It beats vertex, fragment, vertex_s and fragment_s outright: when this is non-blank it is the source for both graphics stages.
var Box.task: String? The task-shader (amplification) stage GLSL of a mesh-shader pipeline. meshModuleHere needs task, mesh and fragment — there is no task-less mesh path here — and fills in whichever of the three the box is missing, each independently and each with its own stub: an EmitMeshTasksEXT amplifier here, a SetMeshOutputsEXT triangle emitter for mesh, and a constant-colour write for fragment. A box that already has a task shader keeps it and gets only the other two.
var Box.mesh: String? The mesh-shader stage GLSL, the stage that emits vertices and primitives directly (SetMeshOutputsEXT / gl_MeshVerticesEXT) instead of consuming a vertex buffer. Paired with Box.task and Box.fragment by meshModuleHere; note the name collides conceptually with Mesh the Kotlin draw object, which is unrelated.
var Box.fragment_s: String? Slang source for the fragment stage. When non-blank it wins over Box.fragment and is transpiled to GLSL by slangc before shaderc ever sees it, so compile errors arrive in Slang's (line): error format — which numberFromErrorMessage does understand.
var Box.vertex_s: String? Slang source for the vertex stage; wins over Box.vertex when non-blank. Prefer Box.vertex_fragment if you want the two stages to share declarations — two separate _s properties are transpiled independently and cannot see each other.
var Box.task_s: String? Slang source for the task stage; wins over Box.task when non-blank.
var Box.mesh_s: String? Slang source for the mesh stage; wins over Box.mesh when non-blank.
var Box._fragment: String? The prepared fragment GLSL: box-local, post-#include-template, post-{% copyPC %}, post-Slang-transpile — literally the text handed to shaderc. Written by every prepare pass and read by _moduleHere, which throws a NullPointerException if no pass ever ran.
var Box._vertex: String? The prepared vertex GLSL — see Box._fragment for what "prepared" means and why you should treat it as read-only.
var Box._task: String? The prepared task-stage GLSL. See Box._fragment.
var Box._mesh: String? The prepared mesh-stage GLSL. See Box._fragment.
var Box.compute: String? The compute-stage GLSL for this box, the source computeModuleHere { } builds from. Its layout(binding = …) buffer blocks and push constants generate the box's ComputeMesh accessors.
var Box.compute_s: String? Slang source for the compute stage; wins over Box.compute when non-blank.
var Box._compute: String? The prepared compute GLSL. See Box._fragment.
var Box.raygen: String? The ray-generation stage GLSL: the one ray-tracing stage that is required, and the only one rayTracingModuleHere seeds a default for (defaultRaygenShader). The others are all optional and are simply skipped if their property is blank.
var Box.miss: String? The miss-shader stage GLSL, run when a traced ray hits nothing. Optional: leave it blank and the stage is left out of the pipeline entirely.
var Box.closesthit: String? The closest-hit stage GLSL. Optional; omitted from the pipeline when blank.
var Box.anyhit: String? The any-hit stage GLSL, run per candidate intersection (alpha-cutout style rejection). Optional; omitted from the pipeline when blank.
var Box.intersection: String? The intersection stage GLSL for custom (non-triangle) primitives. Optional; omitted from the pipeline when blank.
var Box.callable: String? The callable-shader stage GLSL. Optional; omitted from the pipeline when blank.
var Box.raygen_s: String? Slang source for the raygen stage; wins over Box.raygen when non-blank.
var Box.miss_s: String? Slang source for the miss stage; wins over Box.miss when non-blank.
var Box.closesthit_s: String? Slang source for the closest-hit stage; wins over Box.closesthit when non-blank.
var Box.anyhit_s: String? Slang source for the any-hit stage; wins over Box.anyhit when non-blank.
var Box.intersection_s: String? Slang source for the intersection stage; wins over Box.intersection when non-blank.
var Box.callable_s: String? Slang source for the callable stage; wins over Box.callable when non-blank.
var Box._raygen: String? The prepared raygen GLSL. See Box._fragment.
var Box._miss: String? The prepared miss GLSL, or null when the box has no miss stage. See Box._fragment.
var Box._closesthit: String? The prepared closest-hit GLSL, or null when the box has no such stage. See Box._fragment.
var Box._anyhit: String? The prepared any-hit GLSL, or null when the box has no such stage. See Box._fragment.
var Box._intersection: String? The prepared intersection GLSL, or null when the box has no such stage. See Box._fragment.
var Box._callable: String? The prepared callable GLSL, or null when the box has no such stage. See Box._fragment.
var Box._shader: ModulePointer? The live graphics module this box owns, or null if it has never built one. Set by Box._moduleHere and Box.attachShader; a re-run replaces the pointer rather than adding a second module.
var Box._computeShader: ComputeModulePointer? The live compute module this box owns, or null. Set by Box._computeModuleHere and Box.attachShader; see Box._shader for the ownership rules.
var Box._rayTracingShader: RayTracingModulePointer? The live ray-tracing module this box owns, or null. Set by Box._rayTracingModuleHere and Box.attachShader; see Box._shader.
var Box.meshShader: MeshModulePointer? Vestigial: declared, inherited, and read or written by nothing in the tree. The mesh pipeline uses the box-local Box._meshShader throughout; treat this as a name to avoid rather than an API.
var Box._meshShader: MeshModulePointer? The live mesh-shader module this box owns, or null. Set by Box._meshModuleHere and Box.attachShader; see Box._shader.
var Box._fieldProbe: ShaderFieldProbe? This box's GPU sample probe, or null if nothing has asked for one. Created on demand either by Box.fieldProbeHere or by the first #include "fieldProbe" in one of this box's shaders, so the include alone is enough to bring it into being.
var Box._fieldProbeAutoController: ShaderFieldAutoController? The per-frame controller installed by Box.fieldProbeAutoControl, or null. Kept on the box so a re-run can stop the previous one before installing its replacement — which is exactly what fieldProbeAutoControl does, and why re-running a box does not leave two controllers fighting over the same push constants.
var Box._fieldStatsBinding: ShaderFieldStatsBinding? How this box is joined to a ShaderFieldStats block: the stats object plus whether this box is its producer (it owns the buffers and its GLSL records into them) or merely a consumer.
val Once.vertex by Onceing<String>() once.vertex { …GLSL… } fills the box's Box.vertex property from code, the first time only.
val Once.fragment by Onceing<String>() once.fragment { …GLSL… } fills Box.fragment once from code. Same fragment-ordering trap as Once.vertex: seed it in a fragment before the one that expands moduleHere.
val Once.vertex_fragment by Onceing<String>() once.vertex_fragment { …Slang… } fills Box.vertex_fragment once — the shared-Slang route, which overrides whatever once.vertex/once.fragment put in place.
val Once.compute by Onceing<String>() once.compute { …GLSL… } fills Box.compute once, ahead of the computeModuleHere fragment.
val Once.task by Onceing<String>() once.task { …GLSL… } fills Box.task once, ahead of the meshModuleHere fragment.
val Once.mesh by Onceing<String>() once.mesh { …GLSL… } fills Box.mesh once, ahead of the meshModuleHere fragment.
val Once.raygen by Onceing<String>() once.raygen { …GLSL… } fills Box.raygen once, ahead of the rayTracingModuleHere fragment.
val Once.miss by Onceing<String>() once.miss { …GLSL… } fills Box.miss once; leave it unset to build a pipeline with no miss stage at all.
val Once.closesthit by Onceing<String>() once.closesthit { …GLSL… } fills Box.closesthit once. Optional stage.
val Once.anyhit by Onceing<String>() once.anyhit { …GLSL… } fills Box.anyhit once. Optional stage.
val Once.intersection by Onceing<String>() once.intersection { …GLSL… } fills Box.intersection once. Optional stage.
val Once.callable by Onceing<String>() once.callable { …GLSL… } fills Box.callable once. Optional stage.
val Box.shaderTemplate: MutableMap<String, Any> The named values this box's {{ slot }} template expressions resolve against — the mechanism behind the shadertoy preamble and any GLSL boilerplate you would rather write once.
val Box.stackAutoShaderSources: MutableMap<String, String> StackSL source, by stack.auto slot name, for every {{ stack.auto.foo }} this box's shaders declare. Concurrent, because the compile/reload path reads it off the main thread.
const val STACK_AUTO_REPORT_CHANNEL The box output channel the live-coding path writes a non-StackAutoReport.quiet report to.
val stackAutoShaderUpdated Called on Field's main thread when a box's queued stack.auto pipeline update has finished and nothing newer is queued behind it: true when the new pipeline is in place (the next frame draws it), false when the update failed and the old one still draws. A slot change is only queued when its setter returns; a caller that must see the result waits for this.
val defaultVertexShader The vertex shader a box gets if moduleHere expands and the box has no vertex source of its own: one vec3 pos input transformed by a push_constant block of two mat4s, p and m.
val defaultFragmentShader The fragment shader a box gets if moduleHere expands with no fragment source of its own: a flat warm orange, chosen so "it drew something" is unmistakable. Declares no uniforms, so it generates no accessors.
val shadertoyVertexShader The vertex shader shadertoyHere seeds: no inputs at all, just a full-screen triangle synthesised from gl_VertexIndex. That is why the shadertoy module needs a 3-vertex mesh and no vertex buffer.
val shadertoyFragmentShader The fragment shader shadertoyHere seeds: the {{ shadertoy.image }} slot, which brings in the iResolution/iTime/iMouse/… uniform block, the #defines that make them look like shadertoy globals and the main that calls mainImage, then a placeholder mainImage drawing a uv gradient that pulses with iTime.
var Box.customTextureResolver: ((Box, String) - A hook for deciding yourself what a shader's sampler2D someName should be bound to, consulted before Field's own by-name search of the box's properties.
val defaultRaygenShader The ray-generation shader a box gets when rayTracingModuleHere expands and the box has no raygen source: it writes the launch uv into the ray payload and traces nothing at all.

StackAutoReport

data class StackAutoReport(val name: String, val compiled: Boolean, val problems: List<String> = emptyList(), val warnings: List<String> = emptyList(), val notes: List<String> = emptyList(), val errors: List<String> = emptyList()) : ToHTML

What compiling one stack.auto slot had to say beyond "it compiled" — the warnings, and the stack arithmetic that says whether the program is shaped the way its author thinks.

Declaration Summary
val name: String The slot this is about — the foo of {{ stack.auto.foo }}.
val compiled: Boolean False when the program has an ERROR diagnostic, which is then in errors.
val problems: List<String> Almost certainly bugs. A definition whose net stack effect is not "consume one, leave one".
val warnings: List<String> The compiler's own non-ERROR diagnostics: warnings and notes from a compile that succeeded.
val notes: List<String> Worth seeing, not worth failing on — chiefly how much computed work nothing consumes.
val errors: List<String> Why it did not compile: the compiler's ERROR diagnostics, empty when compiled. warnings leaves errors out, so a report that says why a candidate was refused has to read this list.
val quiet: Boolean Nothing to say: it compiled, no warnings, every definition balanced, nothing pruned.
val lines: List<String> Every line, most serious first, for a log or a one-shot println.
override fun toHTML(): String The report as a small monospaced card: one coloured line per problem, warning and note, and a dim "clean" when there is nothing to say. This is what the stack.auto output channel shows.
override fun toString(): String One line, for a log or a println. "stack.auto.<name>: clean" when there is nothing to say.

StackAutoReportSet

data class StackAutoReportSet(val reports: List<StackAutoReport>) : ToHTML

Every slot of one box that currently has something to say, rendered as one output card.

Declaration Summary
val reports: List<StackAutoReport> Every slot with something to say, ordered by slot name so the card does not reshuffle.
override fun toHTML(): String Each slot's card, stacked, so one output area covers the whole shader.
override fun toString(): String One line per slot.

ShaderIncludeRootRequest

data class ShaderIncludeRootRequest(val root: String, val includePath: String, val requestedPath: String, val requestingSourcePath: String?, val includeType: ShaderIncludeType, val includeDepth: Long)

One #include that landed inside a registered include root, handed to a ShaderIncludeRootResolver to turn into text.

Declaration Summary
val root: String The registered root name that matched — the first path segment of a root/… include, or, for a relative include, the root the including file itself came from.
val includePath: String The path within root, normalised: separators unified to /, . and empty segments dropped, .. resolved (and rejected if it would escape the root), no leading slash. This is the key a resolver should look up.
val requestedPath: String Exactly what the #include said, root prefix and all. Useful in diagnostics; do not resolve against it.
val requestingSourcePath: String? The resolved path of the file doing the including (a previous resolution's resolvedPath, or a field/<box>-<id>/<property>.glsl name for box source), or null when shaderc did not supply one.
val includeType: ShaderIncludeType RELATIVE for #include "…", STANDARD for #include <…>. Only RELATIVE includes are resolved relative to requestingSourcePath's directory.
val includeDepth: Long How deep this include is nested, straight from shaderc. Informational — nothing in Field enforces a limit on it.

ShaderIncludeRootResolver

fun interface ShaderIncludeRootResolver

Turns an include inside one registered root into source text. Register one under a name with registerShaderIncludeRoot and every #include "thatName/whatever.glsl" in every box's shaders goes through it.

Declaration Summary
fun resolve(request: ShaderIncludeRootRequest): ShaderIncludeResolution? Resolve one include, or return null if this root does not have it (which the caller turns into a hard error).

ShaderRemoteIncludeCacheOptions

data class ShaderRemoteIncludeCacheOptions(val cacheDirectory: Path, val connectTimeoutMillis: Int = 5_000, val readTimeoutMillis: Int = 5_000)

Where a URL-backed include root keeps its on-disk copies, and how patient it is with the network.

Declaration Summary
val cacheDirectory: Path Directory the fetched includes are mirrored into, one file per include path. Created on demand. Resolved to an absolute, normalised path, and includes that would escape it are rejected — a remote server cannot talk you into writing outside this directory.
val connectTimeoutMillis: Int HttpURLConnection connect timeout, in milliseconds.
val readTimeoutMillis: Int HttpURLConnection read timeout, in milliseconds. Both timeouts are felt by whichever thread is compiling — the main runloop, on the reload path — so the defaults are short.

ShaderProbeAutoControlConfig

data class ShaderProbeAutoControlConfig(val minSamples: Int = 128, val clipLower: Double? = 0.0, val clipUpper: Double? = 1.0, val clipEpsilon: Double = 1.0e-6, val useNominalParams: Boolean = false, val displayChannel: String? = "auto", val closeDisplayOnStop: Boolean = false, val clearDisplayOnStop: Boolean = false, val runloopKeySuffix: String = "shaderFieldAuto")

How Box.fieldProbeAutoControl turns probe read-backs into controller updates: when a batch is big enough to act on, what counts as clipped, and where the diagnostics card goes.

Declaration Summary
val minSamples: Int Minimum samples in a read-back before the controller is allowed to learn from it. A snapshot with fewer is skipped entirely — tick() returns null and nothing is applied.
val clipLower: Double? Values at or below this (within clipEpsilon) count as clipped low and are reported to the controller as a clip rate; null means "nothing clips at the bottom".
val clipUpper: Double? Values at or above this (within clipEpsilon) count as clipped high; null means nothing clips at the top. With both bounds null the reported clip rate is a flat 0.0.
val clipEpsilon: Double Tolerance for the two clip comparisons — floating-point slop, not a percentage.
val useNominalParams: Boolean true applies the controller's nominal parameters, false (the default) its applied ones, which carry the small dither the controller needs to identify the system. Turn it on once identification has settled and the dither is visible in the output.
val displayChannel: String? Box output channel for the HTML diagnostics table (nominal vs applied parameters, objective, confidence, clip rate, p10/p50/p90). null disables the display entirely, which is what a headless test wants.
val closeDisplayOnStop: Boolean Whether stop() closes the display channel by default.
val clearDisplayOnStop: Boolean Whether stop() clears the display channel's contents by default.
val runloopKeySuffix: String Prefix of the runloop hook key, which is "$runloopKeySuffix:${box.id}". Two controllers on one box need two different values here or the second silently displaces the first.

ShaderFieldAutoController

class ShaderFieldAutoController(val box: Box, val probe: ShaderFieldProbe, val controller: AdaptiveDistributionController, val config: ShaderProbeAutoControlConfig, private val apply: (params: DoubleArray, output: ControlOutput, batch: FieldProbeScalarBatch) -> Unit)

The running loop that closes a shader's feedback circuit: read the probe, push the samples into an adaptive controller, hand the new parameters to your apply lambda, and draw the diagnostics.

Declaration Summary
val box: Box The box this controller belongs to: the source of the display channel and the identity in the runloop hook key.
val probe: ShaderFieldProbe The probe being read. Box.fieldProbeAutoControl has already put it into continuous-read mode, so it delivers a snapshot every frame rather than at the display rate.
val controller: AdaptiveDistributionController The adaptive controller doing the actual learning; its parameterNames label the diagnostics table and its pushSamples is what turns a batch into parameters.
val config: ShaderProbeAutoControlConfig The thresholds, clip bounds and display settings this controller was built with. Read on every tick, but the object is immutable — copy it and rebuild to change your mind.
var tickCount: Int How many ticks have actually done work. Ticks that found no new snapshot, or too few samples, do not count — so a tickCount stuck at zero means the probe is not delivering, not that the hook is missing.
var lastBatch: FieldProbeScalarBatch? The last probe batch the controller learned from, or null before the first useful tick. Carries both the raw snapshot and the clip rate computed from config.
var lastOutput: ControlOutput? The last controller output — nominal and applied parameters plus diagnostics — or null before the first useful tick. The same object the apply lambda was handed.
fun tick(): ControlOutput? Do one control step, returning the controller's output, or null if there was nothing to do.
fun install(runloop: Runloop = Runloop.runLoop): ShaderFieldAutoController Install a preRun hook that calls tick once per frame, and return this for chaining. Idempotent — a second call while already installed does nothing.
fun stop(runloop: Runloop = Runloop.runLoop, closeDisplay: Boolean = config.closeDisplayOnStop, clearDisplay: Boolean = config.clearDisplayOnStop): ShaderFieldAutoController Remove the runloop hook, optionally tidy the display channel, and forget this controller on the box. Returns this, and can be re-installed afterwards.

ShadertoySurface

class ShadertoySurface(val size: () -> Pair<Int, Int>, val pointer: () -> Pair<Pair<Float, Float>, Boolean>?)

What a shadertoy module draws onto, for its iResolution and iMouse: size in pixels, and pointer, the cursor in those pixels with y up (null when there is no cursor over it) plus whether the left button is down.

Declaration Summary
val size: () -> Pair<Int, Int>
val pointer: () -> Pair<Pair<Float, Float>, Boolean>?

SlugBuffer.kt

The GPU side of Slug filled-path rendering: SlugBuffer holds one SlugPath's atlas words in a Vulkan storage buffer that a slug shader reads.

SlugBuffer.replace swaps in new geometry behind the same buffer supplier, so existing shader bindings keep working, and SlugBuffer.close falls back to a shared empty atlas that draws nothing. SlugRenderSmoke exercises it against a live renderer.

src/jvmMain/kotlin/afield/graphics/text/slug/SlugBuffer.kt — read the source

SlugBuffer

class SlugBuffer(private val renderer: Renderer, path: SlugPath) : HasBuffer, AutoCloseable

An immutable Slug atlas upload behind a stable address supplier.

Declaration Summary
var isClosed: Boolean
var revision: Long
fun replace(path: SlugPath) Publish newly prepared geometry; existing shader bindings keep the same supplier.
override fun getBuffer(): Long
override fun getGPUAddress(cmd: CommandBuffer): Long
override fun close() Retire the atlas. A mesh still bound to this buffer draws nothing from the next frame on, so it does not matter whether the mesh is removed first. Repeated close calls are harmless.

SlugShaders

object SlugShaders

Shader sources shared by the documented box and shader/GPU regression tests.

Declaration Summary
val vertex: String
val fragment: String

SlugPath.kt

Compiles a filled, flat FLine into Slughorn atlas data (SlugPath) by running the native field-slug helper, built with ./gradlew buildSlugNative.

SlugPath.compile runs once per geometry edit, not per frame, and refuses paths that leave z=0. The result is immutable and carries no placement; SlugBuffer uploads it to the GPU. SlugPathTest covers it.

src/jvmMain/kotlin/afield/graphics/text/slug/SlugPath.kt — read the source

SlugPath

class SlugPath private constructor(private val packed: IntArray)

One filled XY path compiled into immutable Slughorn data. Placement is external.

Declaration Summary
val words: IntArray A defensive copy; mutating it cannot change a validated path.
val curveCount: Int
val byteSize: Long

typography.kt

Text as geometry: a string becomes contours, and from there everything Field already does to geometry applies to letters.

A layout here is a value you can extrude, march, sample or animate, not a label painted into a raster — which is what every other use of java.awt.Font in the tree does (MeshGallery, ImagingGallery, montage's 5x7 bitmap font). It needs no dependency beyond AWT: Font.createGlyphVector(...).getOutline() hands back real quadratic/cubic glyph outlines, and it works headless. typography_field.kt is the bridge to Field's own 2D and 3D outputs.

The three values

  • Typeface — a font at a size, from a logical/system name (typeface) or a .ttf/.otf on disk (typefaceFromFile). Carries the vertical metrics (ascent, descent, leading) a layout needs.
  • Glyph — one glyph's outline, cached per typeface and glyph code, in its own frame with its origin on its own baseline. Both representations live side by side: Glyph.contours are flattened, nesting-classified, winding-normalised polygons for geometry work, and Glyph.outline is the same rings as an FLine of real cubics for drawing.
  • TextLayout — glyphs placed on baselines by AWT's own advances and kerning, plus tracking, multi-line and alignment. Every glyph in it is a PlacedGlyph carrying mutable transform handles (offset, rotation, scale, pivot). Those handles are how you animate letters individually, and TextLayout.onCurve is nothing but a routine that sets them from an FLine's arc length.

Text space

Glyph and layout coordinates are x right, y down, origin at the first baseline, units = the typeface's size in points. That is AWT's own glyph frame and it is also the frame Field's 2D outputs use (Rect's y grows downwards), so text handed straight to Box.lines or Box.showText reads the right way up with no transform. The 3D bridges in typography_field.kt flip y at their own boundary, because a mesh wants y up.

The winding convention

A glyph's counter — the hole in o, e, a, B — is only a hole if something says so, and what the font shipped is not enough to go on. (isoContourFLines in contours.kt leaves winding arbitrary, and every consumer downstream has to cope.) So this file decides:

For a ring p0..pn, signed area is ½Σ(xᵢyᵢ₊₁ − xᵢ₊₁yᵢ) in text space. Outer contours are positive; holes are negative. A ring's GlyphContour.depth — how many other rings of the same glyph contain it — is authoritative: even depth is an outer contour, odd depth is a hole. A ring whose area sign disagrees with its depth is reversed on the way out.

Both representations obey it, so contours.filter { it.isHole } and the subpaths of Glyph.outline agree, and TypographyTest asserts it on glyphs whose answer is known by eye (o has one hole, l has none, % has two).

AWT and the main run loop

imaging_field.kt captions montage with a bitmap table instead of Graphics2D.drawString, for a reason that reaches this file: on macOS, touching the AWT font manager pulls in AppKit, which competes with GLFW for the main run loop, and Server.main never initialises the toolkit. This family cannot avoid the font manager — it is the font manager — so it contains the hazard in one place, Typefaces.warm, whose KDoc carries the measured stack trace and the conclusion:

A JVM that wants glyph outlines and passes -XstartOnFirstThread must also pass -Djava.awt.headless=true. Otherwise CFontManager.loadFonts dispatches to the process's first thread and waits forever for a run loop nobody is pumping.

Typefaces.warm detects that in four seconds and throws with that sentence in it, rather than hanging the editor. Every jvm-main gallery in the tree already sets the property in the first line of main, jvmTest does not need it (Gradle's test JVM has no -XstartOnFirstThread), and an interactive Field window pumps its run loop through glfwPollEvents and so is expected to be fine — expected, not tested; see Typefaces.warm. Everything after that one touch is pure computation over cached outlines.

src/jvmMain/kotlin/afield/graphics/text/typography.kt — read the source

Used in: Typography as geometry — a string becomes contours, A Word Cut as a Stencil, Datalog in a box

Top-level functions

Declaration Summary
fun typeface(name: String = Font.SANS_SERIF, size: Number = 64.0, bold: Boolean = false, italic: Boolean = false): Typeface A logical or installed font by name — the short form of Typefaces.named.
fun typefaceFromFile(path: String, size: Number = 64.0): Typeface A .ttf/.otf from disk — the short form of Typefaces.fromFile.
fun layoutText(text: String, typeface: Typeface, tracking: Double = 0.0, lineSpacing: Double = 1.0, align: TextAlign = TextAlign.LEFT, flatness: Double = typeface.size / 400.0): TextLayout Lay a string out: AWT advances and kerning, plus tracking, line breaks and alignment.
fun Typeface.layout(text: String, tracking: Double = 0.0, lineSpacing: Double = 1.0, align: TextAlign = TextAlign.LEFT): TextLayout layoutText with the typeface first — reads better when you keep a typeface around.
fun TextLayout.onCurve(path: FLine, startOffset: Double = 0.0, align: TextAlign = this.align, flip: Boolean = false, clampToPath: Boolean = true): TextLayout Bend a layout onto an FLine by arc length.
fun textCircle(radius: Number, centre: Vec2 = Vec2(0f, 0f), clockwise: Boolean = true, segments: Int = 96): FLine A circle as an FLine, clockwise from the top — the path onCurve wants for a roundel.
fun textArc(width: Number, height: Number, from: Vec2 = Vec2(0f, 0f)): FLine A cubic arch through three points — a quick path for onCurve when you do not want to draw one.
fun FLine.arcLength(): Double Total arc length of a path, the number onCurve's startOffset is measured in.
fun wrapText(text: String, typeface: Typeface, maxWidth: Double, tracking: Double = 0.0): String The classic single-line word-wrap, in text-space units.
fun textWidth(text: String, typeface: Typeface, tracking: Double = 0.0): Double Advance width of a string without building a layout you keep.
fun GlyphContour.resample(spacing: Double): List<Vec2> Even samples along a ring, by arc length — the primitive the point-cloud bridge is built from.
fun GlyphContour.distanceTo(p: Vec2): Double Straight-line distance from p to this ring's boundary. Always non-negative.

Typeface

class Typeface internal constructor(val awtFont: Font)

A font at a size: the thing you ask for glyphs and layouts.

Declaration Summary
val awtFont: Font The AWT font, exposed: anything AWT can do to a font (attributes, transforms, deriveFont) is available, and Typeface.of wraps the result back up.
val size: Double Point size, as a Double — awtFont.size truncates to Int.
val familyName: String e.g. Helvetica.
val fontName: String e.g. Helvetica-Bold, which is what a .ttf reports and what round-trips through AWT.
val ascent: Double Distance from the baseline up to the top of most glyphs, positive, in text-space units.
val descent: Double Distance from the baseline down to the bottom of descenders, positive.
val leading: Double The font's recommended extra space between lines, positive.
val lineHeight: Double ascent + descent + leading — the natural baseline-to-baseline distance at this size.
val kerning: Boolean Whether AWT will apply this font's kerning pairs during layout.
fun atSize(size: Number): Typeface The same font at a different size.
fun withKerning(on: Boolean): Typeface The same font with AWT's kerning-pair lookup switched on or off.
fun withStyle(bold: Boolean = false, italic: Boolean = false): Typeface The same font emboldened / italicised through AWT's style bits (synthetic if the family lacks it).
fun canDisplay(text: String): Boolean True when this font has a glyph for every character of text.
fun glyph(c: Char): Glyph One glyph's outline, by character. Convenience over layoutText for a single letter.
override fun toString(): String
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

Typefaces

object Typefaces

Font loading, the glyph cache, and the one dangerous moment in this family.

Declaration Summary
val isWarm: Boolean True once warm has succeeded, so every later layout is pure computation. A caller on the main run loop can check this to avoid the stall warm describes, and warm from another thread instead.
fun warm() Initialise the AWT font manager once, on a helper thread, and refuse to hang if it will not answer.
var lastWarmStall: String? Where the last stalled warm attempt was, top frames first.
fun named(name: String = Font.SANS_SERIF, size: Number = 64.0, bold: Boolean = false, italic: Boolean = false, kerning: Boolean = true, strict: Boolean = false): Typeface A logical or installed font by name.
fun fromFile(path: String, size: Number = 64.0, kerning: Boolean = true): Typeface A .ttf / .otf / .ttc from disk, through Font.createFont(TRUETYPE_FONT, file).
fun available(): List<String> Every font family the graphics environment can see, sorted.
fun firstAvailable(vararg candidates: String, size: Number = 64.0, fallback: String = Font.SANS_SERIF): Typeface The first of candidates that is actually installed, else fallback.
fun clearCache() Drop every extracted outline. Only useful if you are churning through fonts from disk.
val cachedGlyphCount: Int How many glyph outlines are currently cached — the number TypographyTest checks caching by.

GlyphContour

class GlyphContour internal constructor(val points: List<Vec2>, val depth: Int)

One closed ring of a glyph outline, flattened to a polygon and classified.

Declaration Summary
val points: List<Vec2> The ring as a polygon in text space, flattened to the tolerance the extraction asked for, and wound so that signedArea agrees with depth. The first point is not repeated at the end.
val depth: Int How many other rings of the same glyph contain this one. 0 for the bowl of o, 1 for its counter. Even is an outer contour, odd is a hole.
val signedArea: Double ½Σ(xᵢyᵢ₊₁ − xᵢ₊₁yᵢ) in text space; positive for outer rings and negative for holes, per the convention this file enforces. Its magnitude is the ring's area in square text-space units.
val isHole: Boolean True for the counter of an o — an odd nesting depth, and so a negative signedArea.
val isOuter: Boolean True for the bowl of an o — an even nesting depth, and so a positive signedArea.
val bounds: Rect Tight bounds in text space.
val perimeter: Double Closed-ring perimeter, in text-space units.
fun toPolyline(): FLine This ring as its own closed FLine — a polyline, not the original cubics.
fun contains(p: Vec2): Boolean Winding-number test against this ring alone, ignoring nesting.
override fun toString(): String

Glyph

class Glyph internal constructor(val glyphCode: Int, val chars: String, val advance: Double, val contours: List<GlyphContour>, val outline: FLine, val bounds: Rect)

One glyph's outline, in its own frame with the origin on its own baseline.

Declaration Summary
val glyphCode: Int The font's own glyph id, not a character code: a ligature has one code and several characters, and the same code means different letters in different fonts.
val chars: String The characters that produced this glyph the first time it was extracted.
val advance: Double How far the pen moves after this glyph, in text-space units. Whitespace has an advance and no contours.
val contours: List<GlyphContour> The rings as polygons, in the font's own order, depth-classified and winding-normalised. This is what the mesh, SDF, raster and point-sampling bridges consume.
val outline: FLine The same rings in the same order, one moveTo-led subpath each, keeping the font's real cubics. This is what you draw, and its subpaths run in step with contours.
val bounds: Rect Tight bounds of the outline (an empty rect at the origin for whitespace), which is the visual box: advance is generally wider, and italics can overhang it.
val isBlank: Boolean True for a space, a tab, or any glyph the font draws nothing for.
val contourCount: Int Rings in total.
val holeCount: Int Rings at odd nesting depth: 1 for o, 2 for %, 0 for l.
val outerCount: Int Rings at even nesting depth: 1 for o, 2 for i (stem and tittle).
val inkArea: Double Ink area: outer rings minus holes. Always >= 0 for a well-formed glyph.
override fun toString(): String

TextAlign

enum class TextAlign

Where a line's glyphs sit relative to the widest line of the layout.

Constants: LEFT, CENTER, RIGHT.

PlacedGlyph

class PlacedGlyph internal constructor(val glyph: Glyph, val origin: Vec2, val lineIndex: Int, val charIndex: Int)

One glyph placed on a baseline, plus the handles that make it kinetic.

Declaration Summary
val glyph: Glyph The outline being placed, shared with every other placement of the same glyph in the same font and size. Do not expect to own it.
val origin: Vec2 Where layout put this glyph, in the layout's text space: on its line's baseline, with the alignment shift already in it. Fixed for the life of the placement — offset is the handle that moves it.
val lineIndex: Int Which of TextLayout.lines this glyph is on, counting from 0.
val charIndex: Int Index into the laid-out string of the first character this glyph renders. Line breaks count, so this indexes TextLayout.text rather than TextLine.text.
var offset: Vec2 Extra translation, in text-space units, applied after layout.
var rotation: Double Rotation about pivot, radians, positive clockwise on screen (because y is down).
var scale: Vec2 Scale about pivot. Negative components mirror, and the geometry bridges cope.
var pivot: Vec2? Rotation/scale centre in the glyph's own coordinates; null means the visual-bounds centre.
var color: Vec4? Per-glyph colour override for TextLayout.toFLines; null means take the caller's colour.
val text: String The characters this placement renders — one, or several for a ligature.
val resolvedPivot: Vec2 The resolved pivot, in glyph coordinates.
fun transform(): AffineTransform The affine this placement's geometry goes through. A fresh instance each call.
val isUntransformed: Boolean True when nothing has been asked of the handles, so the transform is a pure translation.
override fun toString(): String

TextLine

class TextLine internal constructor(val index: Int, val text: String, val baselineY: Double, val advanceWidth: Double, val glyphs: List<PlacedGlyph>)

One baseline's worth of a layout.

Declaration Summary
val index: Int Which line this is, counting from 0, and the PlacedGlyph.lineIndex its glyphs carry.
val text: String This line's own characters, without the line break that ended it.
val baselineY: Double This line's baseline in the layout's text space: index times the layout's TextLayout.lineHeight, so the first line's baseline is at 0 and later lines are at increasing y, which is downwards.
val advanceWidth: Double How far the pen travelled across this line, tracking between glyphs included and the gap after the last glyph excluded. It is not the inked width — an italic f can overhang it.
val glyphs: List<PlacedGlyph> The glyphs on this baseline, in reading order. They are the same objects as the matching stretch of TextLayout.glyphs, so a handle set through either is set for both.

TextLayout

class TextLayout internal constructor(val text: String, val typeface: Typeface, val glyphs: List<PlacedGlyph>, val lines: List<TextLine>, val tracking: Double, val lineHeight: Double, val align: TextAlign)

A laid-out string: glyphs on baselines, with handles.

Declaration Summary
val text: String The string that was laid out, line breaks and all.
val typeface: Typeface The typeface it was laid out in, which is where ascent, descent and the glyphs come from.
val glyphs: List<PlacedGlyph> Every placement in the layout, in reading order across all lines. This is what eachGlyph walks and what the geometry bridges read.
val lines: List<TextLine> The lines, in order, one per \n-separated run of TextLayout.text, empty lines included.
val tracking: Double The extra advance after every glyph that was asked for, in text-space units.
val lineHeight: Double Baseline-to-baseline distance in text-space units — the resolved value, typeface.lineHeight times the lineSpacing layoutText was given, not the multiplier.
val align: TextAlign Where each line sits relative to the widest one.
val ascent: Double Positive, from the typeface.
val descent: Double Positive, from the typeface.
val advanceWidth: Double The widest line's advance width. Tracking is included; the trailing gap is not.
val layoutHeight: Double Baseline-to-baseline of the first and last line, plus one line's ascent and descent.
val layoutBounds: Rect The em box: the rectangle layout reserved, ignoring what the glyphs actually inked.
val bounds: Rect Tight bounds of the inked geometry with every handle applied. Empty for whitespace-only text.
val inkedGlyphs: List<PlacedGlyph> Every glyph that inks something. Whitespace is in glyphs and not here.
val contourCount: Int Total contour count across the layout — outer rings plus holes.
val holeCount: Int Total hole count across the layout.
fun eachGlyph(block: PlacedGlyph.(Int) -> Unit): TextLayout Set the handles glyph by glyph, and hand the layout back for chaining.
fun resetHandles(): TextLayout Reset every handle to its identity.
fun copyLayout(): TextLayout A deep copy: same structure, independent handles.
override fun toString(): String

typography_field.kt

The bridges out of a TextLayout and into the four families Field already has.

typography.kt answers with structure — rings, depths, advances, handles — and structure is not yet a drawing, a mesh, an image or a point cloud. This file converts it: once a string is contours, letters are inputs to machinery that never knew about text.

It is the typographic counterpart of spatial_field.kt (plane geometry → FLine) and mesh_field.kt (TriMesh → FLine), and it is written to the same rule: every function here returns a value — no Box, no output — except the two figure helpers at the bottom, which are drawToSVG and so work headless, in a test and in the docs build.

Five bridges:

  1. → FLine (TextLayout.toFLines, TextLayout.toFLine, Glyph.toFLine). One line per glyph, so a per-glyph handle actually moves something, with the font's real cubics intact. Filling works because of typography.kt's winding convention and nothing else: Skia and Java2D both fill non-zero, so a counter is a hole precisely because its ring runs the other way. TextLayout.toFLines takes honourHoles = false to show you the blob you get without a convention, which is the control arm the docs figure uses.
  2. → TriMesh (TextLayout.toMesh). Hole-aware extrusion: extrudeMesh and triangulatePolygon each take a single ring and cannot do holes, so the caps here go through FloatEarcut with hole indices and the walls are emitted ring by ring from the same flattened vertices, which is what makes the result watertight — and makes an extruded o genus 1 and an extruded l genus 0.
  3. → an SDF (TextLayout.toSdf). A 2D signed-distance grid, bilinearly sampled and extruded in z, so letters compose with sdfSphere, sdfSmoothUnion, .twisted() and everything else in mesh_sdf.kt, and marchingCubes will mesh the result.
  4. → Img (rasterizeText). An analytic even-odd scanline fill of the layout's own rings — not Graphics2D.drawString, both because a stdlib call every box might make must not be able to initialise AWT's font manager and because filling our own outlines means a legible PNG is evidence about this code rather than about AWT's rasteriser.
  5. → points (TextLayout.samplePoints, TextLayout.toPointFLine). Even-by-arc-length samples along the outlines, which is what the alife agents, the clusterers and the contourers in clustering.kt / alife_agents.kt consume.

Frames: bridges 1, 3, 4 and 5 stay in text space (y down, origin on the first baseline); bridge 2 flips y at its boundary, because a mesh wants y up, and re-normalises winding when it does.

src/jvmMain/kotlin/afield/graphics/text/typography_field.kt — read the source

Used in: Typography as geometry — a string becomes contours

Top-level functions

Declaration Summary
fun Glyph.toFLine(color: Vec4 = vec(1.0, 1.0, 1.0, 0.9), thickness: Double = 1.4, filled: Boolean = false, stroked: Boolean = true): FLine This glyph's outline as a drawable FLine, with the font's cubics kept.
fun TextLayout.toFLines(color: Vec4 = vec(1.0, 1.0, 1.0, 0.9), thickness: Double = 1.4, filled: Boolean = false, stroked: Boolean = true, honourHoles: Boolean = true): List<FLine> One FLine per inked glyph, each with that glyph's handles applied.
fun TextLayout.toFLine(color: Vec4 = vec(1.0, 1.0, 1.0, 0.9), thickness: Double = 1.4, filled: Boolean = false, stroked: Boolean = true): FLine The whole layout as a single FLine — every glyph's every ring as subpaths of one line.
fun TextLayout.toContours(honourHoles: Boolean = true): List<GlyphContour> Every ring of the layout, with the handles applied, still classified.
fun TextLayout.samplePoints(spacing: Double = typeface.size / 20.0): List<Vec2> Even-by-arc-length samples along every outline of the layout.
fun TextLayout.toPointFLine(spacing: Double = typeface.size / 20.0, color: Vec4 = vec(0.55, 0.82, 0.95, 0.9), pointSize: Double = 0.6): FLine samplePoints as a dot-drawn FLine, via spatial_field.kt's pointCloudFLine.
fun TextLayout.toMesh(depth: Number = typeface.size * 0.25, yUp: Boolean = true, centre: Boolean = false): TriMesh Extrude the layout into a watertight TriMesh, counters included.
fun TextLayout.distanceField(resolution: Int = 256, margin: Double = 0.15): TextDistanceField Build a TextDistanceField over this layout.
fun TextLayout.toSdf(depth: Number = 0.25, gridResolution: Int = 320, margin: Double = 0.2): TextSdf A layout as a 3D signed-distance field, ready for marchingCubes and every combinator in mesh_sdf.kt.
fun rasterizeText(layout: TextLayout, width: Int, height: Int, margin: Double = 0.06, supersample: Int = 4, window: Rect? = null, fillRule: TextFillRule = TextFillRule.NON_ZERO, rings: List<GlyphContour> = layout.toContours()): Img Rasterise a layout by filling its own rings: an analytic-in-x, supersampled-in-y scanline fill.
fun TextLayout.toImg(width: Int, height: Int, margin: Double = 0.06, supersample: Int = 4, window: Rect? = null, fillRule: TextFillRule = TextFillRule.NON_ZERO): Img rasterizeText with the layout as receiver.
fun Img.inkFraction(threshold: Float = 0.5f): Double What fraction of the raster the ink covers.
fun Box.showText(layout: TextLayout, channel: String = "main", title: String? = null, caption: String? = null, width: Int = 720, height: Int = 260, filled: Boolean = true, stroked: Boolean = false, color: Vec4 = vec(0.93, 0.96, 1.0, 1.0), showBaseline: Boolean = false) A titled figure of a layout in a box output — drawToSVG, so no AWT rasteriser and no GPU.
fun Box.showTextGrid(vararg panels: Pair<String, TextLayout>, channel: String = "main", columns: Int = 2, cellWidth: Int = 420, cellHeight: Int = -1, filled: Boolean = true, stroked: Boolean = false, color: Vec4 = vec(0.93, 0.96, 1.0, 1.0)) Several showText figures side by side; label may carry a caption after a newline.
fun TextLayout.metricsFLines(baselineColor: Vec4 = vec(0.35, 0.75, 0.95, 0.65), guideColor: Vec4 = vec(0.45, 0.52, 0.62, 0.4)): List<FLine> The layout's metric lines — baseline, ascent, descent, and the advance box — as drawable FLines.
fun TextLayout.advanceBoxFLines(color: Vec4 = vec(0.95, 0.62, 0.35, 0.55)): List<FLine> A per-glyph advance-box diagram: one rectangle per glyph, at its handles.

TextDistanceField

class TextDistanceField internal constructor(val bounds: Rect, val nx: Int, val ny: Int, internal val values: FloatArray)

A signed-distance grid over a layout, in text space, negative inside the ink.

Declaration Summary
val bounds: Rect The region the grid covers, in text space: the layout's inked bounds grown by the margin the build asked for. Samples sit on its edges, not half a cell inside them.
val nx: Int Samples across TextDistanceField.bounds, at least 4.
val ny: Int Samples down TextDistanceField.bounds, at least 4. Only the longer side of the grid gets the resolution that was asked for; the shorter one is scaled to keep the cells square.
fun at(x: Double, y: Double): Double Bilinear sample. Outside TextDistanceField.bounds the nearest edge value is used plus the distance to the box — which keeps the field monotone far away, so a marcher never finds a spurious surface out there.
fun at(p: Vec2): Double Bilinear sample at a point.
fun toImg(): Img The grid as a one-channel Img, distance mapped to 0..1 — for looking at the field itself.
fun deepestInside(): Double Largest inscribed distance: how far inside the ink you can get. A stroke-weight measure.

TextSdf

class TextSdf internal constructor(val field: ScalarField3D, val bounds: AxisAlignedBoundingBox3D, val distanceField: TextDistanceField, val scale: Double)

A layout's SDF plus the bounds it lives in — the pair marchingCubes wants.

Declaration Summary
val field: ScalarField3D The field itself, negative inside the letters, in normalised y-up coordinates. Safe to compose with any ScalarField3D.
val bounds: AxisAlignedBoundingBox3D A box comfortably containing the surface, so march needs no arguments.
val distanceField: TextDistanceField The 2D grid underneath, if you want to look at it or measure it.
val scale: Double Normalised units per text-space unit, which is one over the layout's ascent: multiply a text-space length by this to get a normalised one.
fun march(resolution: Int = 96, vertexNormals: Boolean = true): TriMesh March the field. resolution is cells along the longest axis, as in marchingCubes.

TextFillRule

enum class TextFillRule

How a scanline decides whether a crossing count means "inside".

Constants: NON_ZERO, EVEN_ODD.

IOSurfaceHostBuffer.kt

IOSurfaceHostBuffer maps an IOSurface's memory into a Vulkan buffer, so pixels move between an IOSurface and a Vulkan image with one GPU copy on macOS drivers that cannot back an image with an IOSurface.

src/jvmMain/kotlin/afield/graphics/texture/IOSurfaceHostBuffer.kt — read the source

IOSurfaceHostBuffer

class IOSurfaceHostBuffer(private val device: Device, val ioSurfaceRef: Long, private val readOnly: Boolean) : AutoCloseable

An IOSurface's own pages as a VkBuffer, through VK_EXT_external_memory_host.

Declaration Summary
val ioSurfaceRef: Long
val width: Int
val height: Int
val bytesPerRow: Long
val buffer: Long
fun copyToImage(commandBuffer: org.lwjgl.vulkan.VkCommandBuffer, image: Long, bytesPerPixel: Int) Record surface → image (in TRANSFER_DST_OPTIMAL), bytesPerPixel per texel.
fun copyFromImage(commandBuffer: org.lwjgl.vulkan.VkCommandBuffer, image: Long, bytesPerPixel: Int) Record image (in TRANSFER_SRC_OPTIMAL) → surface, bytesPerPixel per texel.
override fun close()

Image.kt

Image wraps a VkImage and its memory, created from an Image.ImageData description, including the external-memory exports used to hand frames to other processes.

Alongside ordinary device-local images, an Image can be allocated for export as an opaque fd (CUDA/NVENC on Linux, see VulkanExternalMemory), as a dma-buf with a DRM format modifier (see LinuxDmaBufExport) or as a Metal IOSurface (see MetalIOSurface). The file also holds the format helpers vkFormatBytesPerPixel and vkFormatBytesPerPixelOrNull, and image_layout_transition for recording layout barriers.

src/jvmMain/kotlin/afield/graphics/texture/Image.kt — read the source

Top-level functions

Declaration Summary
fun vkFormatBytesPerPixelOrNull(format: Int): Int? Bytes occupied by one pixel of format, or null if this table does not know the format.
fun vkFormatBytesPerPixel(format: Int, usage: String): Int vkFormatBytesPerPixelOrNull, throwing for formats it does not know. usage names the caller so the message says which capture or upload failed.
fun image_layout_transition(command_buffer: VkCommandBuffer, image: Image, src_stage_mask: Long, dst_stage_mask: Long, src_access_mask: Long, dst_access_mask: Long, old_layout: Int, new_layout: Int, sub_resource_range: VkImageSubresourceRange, src_queue_family_index: Int = VK_QUEUE_FAMILY_IGNORED, dst_queue_family_index: Int = VK_QUEUE_FAMILY_IGNORED)+1 overload · also Long

Image

class Image(private val device: Device, val imageData: ImageData)

Image wraps a VkImage and its memory, created from an Image.ImageData description, including the external-memory exports used to hand frames to other processes.

Declaration Summary
var format: Int The VkFormat the image was created with, copied from the Image.ImageData it was built from so it can be read back without that description to hand.
var mipLevels: Int How many mip levels the image has, copied from its Image.ImageData. The levels are allocated; filling them is the caller's own blit chain.
var vkImage: Long The VkImage handle. Zero until the constructor has allocated, and zero again after cleanup.
var vkMemory: Long The VkDeviceMemory allocated for vkImage and bound to it. Bound even when the pixels live in an imported IOSurface, where MoltenVK may not need it.
var memorySize: Long The size the driver asked for to back this image, which is the allocation size CUDA's cuImportExternalMemory has to be given when the image is exported as an opaque fd (the Linux NVENC path, see VulkanExternalMemory). Read from the image's memory requirements, so it is the real figure rather than a computed one, and it is set for every image.
var linearRowPitch: Long The row pitch of the image's linear layout, for NVENC's NV_ENC_REGISTER_RESOURCE.pitch. Queried from the image rather than computed as width times bytes per pixel, because the driver may pad a row out to an alignment. Set only for an image whose Image.ImageData asked for exportOpaqueFd, and zero otherwise.
var drmFormatModifier: Long The DRM format modifier the driver chose, which is the tiling an importing process has to be told about before it can read the dma-buf. The driver picks from the modifiers Field offers, so the choice is read back rather than assumed. Set only for an image whose Image.ImageData asked for exportDmaBuf — see LinuxDmaBufExport.
var drmPlaneLayouts: List<DmaBufPlaneLayout> The offset and pitch of each memory plane under the modifier the driver chose, read back alongside drmFormatModifier and sent with the handle so an importer can address the planes.
fun cleanup()
fun setLayoutFor(commandBufferHandle: Long, layout: Int)
fun getLayoutFor(commandBufferHandle: Long): Int?
fun getLastKnownLayout(): Int?
fun exportMetalIOSurfaceRef(): Long Export the IOSurfaceRef backing this image's MTLTexture (MoltenVK, VK_EXT_metal_objects). Requires exportMetalIOSurface set on the Image.ImageData it was created from, and the device to have enabled VK_EXT_metal_objects. Returns the IOSurfaceRef pointer (pass to afield.graphics.texture.MetalIOSurface.ioSurfaceId), or 0 if the surface could not be exported.
fun exportOpaqueFd(): Int Export this image's backing memory as an opaque POSIX fd (Linux, VK_KHR_external_memory_fd, see VulkanExternalMemory). Requires exportOpaqueFd set on the Image.ImageData it was created from. Per the spec each call to vkGetMemoryFdKHR transfers ownership of a new fd to the caller, so this caches and returns the same fd on repeated calls — call close to release it, don't just drop the value. The fd stays valid (and open in this JVM process) for the streamer's whole session; the helper process only ever gets a duplicate via /proc/<pid>/fd/<fd> (see WIRE_FORMAT.md's Linux addendum), never this one.
fun exportDmaBufFd(): Int Export this image's backing memory as a POSIX dma-buf fd (Linux, VK_EXT_external_memory_dma_buf, see LinuxDmaBufExport). Requires exportDmaBuf set on the Image.ImageData it was created from.
class ImageData

ImageView.kt

ImageView wraps a VkImageView over an image, and the small interfaces Field's shader bindings use to find views and samplers.

HasImageView and HasImageViewArray are what a texture binding accepts; PreparesSampledImages lets a source record its layout transitions before sampling, and HasDefaultTextureSampler carries the sampler a binding uses when none is given.

src/jvmMain/kotlin/afield/graphics/texture/ImageView.kt — read the source

Used in: Field 4 Graphics

HasImageView

interface HasImageView

ImageView wraps a VkImageView over an image, and the small interfaces Field's shader bindings use to find views and samplers.

Declaration Summary
val imageView: ImageView The view to bind. Implementations that follow a ring or a ping-pong swap return a different view on each read, which is what lets a binding made once keep pointing at the current image.

HasImageViewArray

interface HasImageViewArray

Declaration Summary
val imageViewArray: List<HasImageView> The views to bind, in the order the shader's array takes them. Every one is written, so the list's length is the descriptor count the array binding receives; an empty list is refused.

PreparesSampledImages

interface PreparesSampledImages

Declaration Summary
fun prepareSampledImagesFor(command: CommandBuffer)

HasDefaultTextureSampler

interface HasDefaultTextureSampler

Declaration Summary
var defaultSampler: TextureSampler? The sampler a texture binding uses when the module does not name one, or null to leave that to the binding. The ping-pong targets forward this to the target itself, so setting it once covers every view they hand out.

ImageView

class ImageView(private val device: Device, val image: Long, val imageViewData: ImageViewData) : HasImageView

Declaration Summary
val image: Long The VkImage this is a view of. The constructor taking an Image passes its handle and records the image in underlyingImage; this one is for a handle whose owner is elsewhere, a swapchain image among them.
val imageViewData: ImageViewData The view's format, aspect, layer range, mip count and debug name, read once here and kept for anything that needs to know what the view covers.
val aspectMask: Int Which aspects of the image the view covers, from imageViewData — colour, or depth with or without stencil. A combined depth+stencil view cannot be sampled, which is why a sampled depth attachment gets a second, depth-only view.
val mipLevels: Int How many mip levels the view covers, starting at level 0. A view of one level over a mipped image is how a render pass writes a single level.
var vkImageView: Long The VkImageView handle, 0 until the constructor has created it.
var underlyingImage : Image? The Image this views, when the view was made from one. It stays null for a view built over a bare handle, so a caller that needs the image's layout or memory has to hold it separately.
fun cleanup()
fun source(i : Image) : ImageView
override val imageView: ImageView
class ImageViewData

LinuxDmaBufExport.kt

LinuxDmaBufExport exports Vulkan images as Linux dma-bufs described by a DRM format modifier, so another process (such as Electron) can import them without a copy.

LinuxDmaBufExport.shareableModifiers and LinuxDmaBufExport.supportsModifier check which modifiers the device can export for a format; LinuxDmaBufExport.handle and LinuxDmaBufExport.handles package an exported image as a DmaBufHandle of fd, modifier and DmaBufPlaneLayouts. Linux only.

src/jvmMain/kotlin/afield/graphics/texture/LinuxDmaBufExport.kt — read the source

DmaBufPlaneLayout

data class DmaBufPlaneLayout(val offset: Long, val rowPitch: Long)

One DRM-modifier memory plane of a dma-buf image, as the importer must be told it: a byte offset into the allocation and a row pitch. These are memory planes of the modifier's layout, not colour planes of the format.

DmaBufHandle

data class DmaBufHandle(val fd: Int, val modifier: Long, val planes: List<DmaBufPlaneLayout>, val size: Long)

An exported dma-buf image: one POSIX fd owning the allocation, the DRM format modifier that describes its tiling, and every memory plane's layout. This is the same information an Electron nativePixmap carries, and it maps directly onto afield.media.browser.ExternalBrowserImageHandle.LinuxDmaBuf.

Declaration Summary
val fd: Int
val modifier: Long
val planes: List<DmaBufPlaneLayout>
val size: Long

LinuxDmaBufExport

object LinuxDmaBufExport

The Linux/dma-buf counterpart of MetalIOSurface: turns a Field image into the cross-process, cross-vendor handle a dma-buf consumer needs.

Declaration Summary
fun shareableModifiers(device: Device, format: Int, usage: Int, externalFeature: Int = VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT): List<Long> The DRM format modifiers this device can both create usage images with and share as a dma-buf, restricted to single-memory-plane layouts. Asked with vkGetPhysicalDeviceImageFormatProperties2 rather than inferred from drmFormatModifierTilingFeatures, because only the former accounts for the external handle type actually being used.
fun supportsModifier(device: Device, format: Int, usage: Int, modifier: Long, externalFeature: Int): Boolean Whether modifier can back a format/usage image that is shareable as a dma-buf in the direction externalFeature describes (VK_EXTERNAL_MEMORY_FEATURE_EXPORTABLE_BIT to produce one, VK_EXTERNAL_MEMORY_FEATURE_IMPORTABLE_BIT to receive one).
fun handle(image: Image): DmaBufHandle Export image — which must have been created with Image.ImageData.exportDmaBuf — as a fresh dma-buf handle. Each call produces a new fd, so each call yields a handle that a consumer may import (and thereby consume) independently.
fun handles(rtt: RenderToTexture): List<DmaBufHandle> Handles for all of an RTT's internally-created color attachments, in slot order (skipping any slot backed by an external view, which has no Image of its own to export). Mirrors VulkanExternalMemory.handles and MetalIOSurface.ioSurfaceIds.

MetalIOSurfaceExport.kt

MetalIOSurface reaches the macOS IOSurface framework from Kotlin, so a Field render-to-texture image can be shared with another process by IOSurfaceID or Mach port.

It looks up, locks, measures and releases IOSurfaces, and MetalIOSurface.ioSurfaceIds lists the ids behind a afield.graphics.vulkan.targets.RenderToTexture created for IOSurface export. macOS only.

src/jvmMain/kotlin/afield/graphics/texture/MetalIOSurfaceExport.kt — read the source

MetalIOSurface

object MetalIOSurface

Bridges a Field RTT color image to a global IOSurfaceID that the field-encode co-process can attach to with IOSurfaceLookup (see native/field-encode/ and dev_docs/vision_frontend.md -> "The sender seam").

Declaration Summary
const val LOCK_READ_ONLY kIOSurfaceLockReadOnly: the lock's owner promises not to write through the CPU mapping.
fun lock(ioSurfaceRef: Long, options: Int = 0) IOSurfaceLock(ref, options, NULL): pin the surface's CPU mapping so baseAddress is valid. The address is only defined while the surface is locked. Balance with unlock.
fun unlock(ioSurfaceRef: Long, options: Int = 0)
fun baseAddress(ioSurfaceRef: Long): Long
fun bytesPerRow(ioSurfaceRef: Long): Long
fun allocSize(ioSurfaceRef: Long): Long
fun width(ioSurfaceRef: Long): Int
fun height(ioSurfaceRef: Long): Int
fun lookup(id: Long): Long IOSurfaceRef IOSurfaceLookup(IOSurfaceID) — resolve a process-global id back to a local IOSurfaceRef pointer (0 if not found). The inverse of ioSurfaceId: use it to import a surface another process created (e.g. one the field-encode helper allocated and returned), then pass the pointer to RTTParameters.importMetalIOSurface.
fun lookupFromMachPort(port: Int): Long Resolve an IOSurface send right that has already been transferred into this process. The returned IOSurfaceRef is retained; release balances it. This does not consume port.
fun createMachPort(ioSurfaceRef: Long): Int Create a send right for ioSurfaceRef. The returned port name is local to this process; transfer the right in a Mach message rather than sending its integer value over ordinary IPC. The caller owns the right and must eventually call deallocateMachPort.
fun deallocateMachPort(port: Int) Drop this process's send right after lookupFromMachPort retained the IOSurface.
fun release(ioSurfaceRef: Long) Balances the retain performed by lookup or lookupFromMachPort. Call only after every Vulkan object importing the returned IOSurfaceRef has been destroyed.
fun ioSurfaceId(ioSurfaceRef: Long): Long IOSurfaceID IOSurfaceGetID(IOSurfaceRef) — returns the process-global id as an unsigned Long (the raw C return is a uint32). 0 for a null ref.
fun ioSurfaceId(image: Image): Long Export + resolve the global id for a single RTT color image. 0 if export failed.
fun ioSurfaceIds(rtt: RenderToTexture): List<Long> The global IOSurfaceIDs for all of an RTT's internally-created color attachments, in slot order (0 for any slot backed by an external view or that failed to export). This is the list to hand to FieldEncodeProcess.open(surfaces = ...).

TextureSampler.kt

TextureSampler wraps a VkSampler: filtering, mipmap mode, anisotropy and per-axis address modes for sampling a texture in a shader.

src/jvmMain/kotlin/afield/graphics/texture/TextureSampler.kt — read the source

TextureSampler

class TextureSampler(val renderer: Renderer = fromContext(null), mipLevels: Int, anisotropyEnable: Boolean, filter: Int = VK10.VK_FILTER_LINEAR, mipmapMode: Int = if (filter == VK10.VK_FILTER_LINEAR) VK10.VK_SAMPLER_MIPMAP_MODE_LINEAR else VK10.VK_SAMPLER_MIPMAP_MODE_NEAREST, addressModeU: Int = VK10.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, addressModeV: Int = VK10.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, addressModeW: Int = VK10.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE)

TextureSampler wraps a VkSampler: filtering, mipmap mode, anisotropy and per-axis address modes for sampling a texture in a shader.

Declaration Summary
val renderer: Renderer
var vkSampler: Long
fun cleanup()

UnixFdPass.kt

UnixFdPass sends file descriptors to another process over a Unix domain socket with SCM_RIGHTS, which is how Field hands exported GPU memory to its Linux encoder helper.

It calls a separately built JNI helper (native/field-fdpass/), loaded through its own class loader; UnixFdPass.available reports whether that helper is present. Linux only.

src/jvmMain/kotlin/afield/graphics/texture/UnixFdPass.kt — read the source

UnixFdPass

object UnixFdPass

Sends raw file descriptors to another process via SCM_RIGHTS over a Unix domain socket — the mechanism VulkanExternalMemory's field-encode-linux handoff actually needs.

Declaration Summary
val available: Boolean The optional helper must be built before starting an external-memory session.
fun closeFd(fd: Int) Close an owned export descriptor after SCM_RIGHTS duplicated it, or on setup failure.
fun sendFds(socketPath: String, fds: IntArray): Boolean Connect to the AF_UNIX SOCK_STREAM socket at socketPath (a peer must already be listening — see field-encode-linux's Linux open handling) and send every fd in fds in one sendmsg call (one SCM_RIGHTS control message, fds preserved in array order). Returns false on any failure (socket missing/not listening yet, connect refused, send failed) — callers should retry with backoff for the "not listening yet" race rather than fail immediately.

VulkanExternalMemory.kt

Exports a render-to-texture color image as a POSIX file descriptor, so the Linux field-encode-linux helper can import it into CUDA and encode it with NVENC.

The chain: render into an RTT created with exportOpaqueFd = true, then Image.exportOpaqueFd (VK_KHR_external_memory_fd) yields an fd open in this JVM process, and VulkanExternalMemory.handle packages it with the exact allocation size (CUDA's cuImportExternalMemory requires an exact match) and the row pitch (NVENC's NV_ENC_REGISTER_RESOURCE.pitch, which is the image's actual, possibly alignment-padded, linear layout rather than width times bytes per pixel).

The fd number never travels over the JSON control channel, because a raw fd means nothing to another process by value; a Mac IOSurfaceID, by contrast, is a system-wide handle. VulkanExternalMemory.FdHandle.fd feeds UnixFdPass.sendFds, which passes the kernel object to the helper over SCM_RIGHTS, and only size and rowPitch go into the JSON of FieldEncodeProcess.open(fdSurfaces = ...).

vkGetMemoryFdKHR is an ordinary LWJGL Vulkan binding, so the export itself lives on Image; this object is the RenderToTexture-level convenience, the counterpart of MetalIOSurface.ioSurfaceIds on macOS.

Linux/NVIDIA only. The fd stays open in this process for the streamer's session, and the helper receives a duplicate of the kernel object, so this process must not close its own fd while streaming. Design notes: dev_docs/vision_frontend.md, "The sender seam", Linux row.

src/jvmMain/kotlin/afield/graphics/texture/VulkanExternalMemory.kt — read the source

VulkanExternalMemory

object VulkanExternalMemory

Packages an RTT's exported color images as fd handles for the Linux encode helper.

Declaration Summary
data class FdHandle(val fd: Int, val size: Long, val rowPitch: Long) An exported image: its fd, exact allocation size in bytes, and linear rowPitch.
fun handle(image: Image): FdHandle image must have been created with Image.ImageData.exportOpaqueFd = true.
fun handles(rtt: RenderToTexture): List<FdHandle> The fd handles for all of an RTT's internally-created color attachments, in slot order (skipping any slot backed by an external view, which has no Image of its own to export). This is the list to hand to FieldEncodeProcess.open(fdSurfaces = ...).

MegaTexture.kt

A virtual texture over thousands of source images, paged into a fixed GPU atlas on demand.

Shaders report which images and detail levels they need through a feedback buffer; MegaTexture reads it back, asks MegaTextureDecoder for the missing pages, copies them into atlas slots and publishes a page table the shader samples through. MegaTextureConfig sets page size, residency budget and feedback rate, and MegaTextureStats reports per-level residency and decode queues.

src/jvmMain/kotlin/afield/graphics/texture/mega/MegaTexture.kt — read the source

MegaTextureConfig

class MegaTextureConfig

How a MegaTexture is shaped: how big its pages are, how much GPU memory it may hold, how hard it decodes, and what its shader-visible names and bindings are.

Declaration Summary
var minPageSize: Int Edge of a level-0 page, for a square page ladder. Level L is then minPageSize shl L.
var levels: Int How many levels exist. The default 7 gives 8, 16, 32, 64, 128, 256 and 512-pixel pages.
fun sourcePage(width: Int, height: Int): MegaTextureConfig Shape the pages like the source, and make the top level exactly this size.
val topPageWidth: Int Top-level page width: sourcePage's, or the square ladder's minPageSize shl (levels-1).
val topPageHeight: Int Top-level page height. Equal to topPageWidth unless sourcePage said otherwise.
var atlasEdge: Int Edge of one atlas array layer, in pixels. Every level packs whole pages into these.
var layersPerLevel: Int Array layers given to each level above the base. More layers, more pages resident.
var maxBaseLevelLayers: Int Ceiling on layers for level 0, which otherwise gets however many it takes to hold every image at once.
var decodeThreads: Int
var maxQueuedRequests: Int Queued requests beyond this are dropped worst-first at the end of a feedback pass.
var uploadBytesPerFrame: Int Ceiling on page bytes copied into the atlas per frame. Keeps a burst off the frame time.
var feedbackEveryNFrames: Int How often the GPU's request buffer is read and cleared, in frames. 0 means "choose from the image count".
var staleFrames: Int A request not re-asked for in this many frames is abandoned rather than decoded.
var warmUpRequestsPerFrame: Int Base-level requests submitted per frame while warming up.
var warmUpQueueTarget: Int Warm-up pauses while the decode queue is deeper than this, so it cannot outrun the workers.
var warmUpReadyTarget: Int Warm-up also pauses while this many decoded pages are waiting for an upload slot.
var pendingExpiryFrames: Int Backstop: after this many frames an outstanding request is assumed lost regardless.
var evictionGraceFrames: Int Slots not sampled for this many frames may be evicted.
var crossfadeFrames: Int Frames a newly arrived page takes to fade in over the one it replaces. 0 to switch instantly.
var evictionsPerFramePerLevel: Int Evictions started per level per frame when a level is under pressure.
var cacheMaxLevel: Int Highest level written to the on-disk page cache. Above this, pages are re-decoded.
var cacheMaxVirtualBytes: Long A level whose fully-populated cache file would exceed this is not cached at all.
var cacheRoot: Path? Where the catalog and page cache live. Null means MegaTextureCatalog.defaultCacheRoot.
var descriptorSet: Int
var atlasBinding: Int
var pageTableBinding: Int
var feedbackBinding: Int
var atlasName: String GLSL names. Change them if they collide with something already in your shader.
var pageTableName: String
var feedbackName: String
var name: String

MegaTextureStats

data class MegaTextureStats(val imageCount: Int, val frame: Long, val wantedImages: Int, val residentPages: IntArray, val pageCapacity: IntArray, val publishedAtLevel: IntArray, val unpublished: Int, val queueDepth: Int, val inFlightDecodes: Int, val decodedFromCache: Long, val decodedFromSource: Long, val decodeFailures: Long, val cancelled: Long, val uploadedPagesThisFrame: Int, val uploadBytesThisFrame: Int, val effectiveMaxLevel: Int, val warmUpRemaining: Int, val pageWidths: IntArray, val pageHeights: IntArray) : ToHTML

What a MegaTexture is doing right now — for println, an output card, or a test.

Declaration Summary
val imageCount: Int
val frame: Long
val wantedImages: Int
val residentPages: IntArray
val pageCapacity: IntArray
val publishedAtLevel: IntArray
val unpublished: Int
val queueDepth: Int
val inFlightDecodes: Int
val decodedFromCache: Long
val decodedFromSource: Long
val decodeFailures: Long
val cancelled: Long
val uploadedPagesThisFrame: Int
val uploadBytesThisFrame: Int
val effectiveMaxLevel: Int
val warmUpRemaining: Int Images with no base-level page yet.
val pageWidths: IntArray Page width per level, so the table can name each level by the page it actually holds.
val pageHeights: IntArray Page height per level. Equal to pageWidths for a square ladder.
override fun toHTML(): String One row per level, because that is the shape of the question being asked: how sharp is the wall, and is any level out of pages. The ceiling is marked rather than stated separately — a level above it is not being served, and seeing which one is more useful than the number.
override fun toString(): String

MegaTexture

class MegaTexture(val renderer: Renderer, val catalog: MegaTextureCatalog, val config: MegaTextureConfig = MegaTextureConfig()) : AutoCloseable, HasImageViewArray, HasImageView

A GPU-driven virtual texture over an arbitrarily large set of images.

Declaration Summary
val renderer: Renderer
val catalog: MegaTextureCatalog
val config: MegaTextureConfig
val imageCount: Int
val levels: Int
val layout Where every page lives, and how a page table entry is packed. Pure arithmetic, no device — see MegaTextureLayout for why it is a separate object.
val totalLayers: Int
val atlasImage: Image
val atlasView: ImageView
val sampler: TextureSampler
override val imageViewArray: List<HasImageView>
override val imageView: ImageView
val template: Map<String, Any> The {{ mega.… }} slots this system exposes to a box's shaders.
val atlasEdge: Int
fun pageWidth(level: Int): Int
fun pageHeight(level: Int): Int
fun capacityOf(level: Int): Int
fun isIdentityLevel(level: Int): Boolean
fun bind(parameters: Parameters): Parameters Point a module's atlas sampler, page table and feedback buffer at this system.
fun beforeTarget(): BeforeTarget The per-frame work, as a target to hang on a module ahead of its render pass.
fun afterTarget(): Target Make the shader's feedback writes readable by the host once the render pass has ended.
fun attach(module: ModulePointer, name: String = config.name): MegaTexture Register beforeTarget and afterTarget on module under name.
fun warmBaseLevel(level: Int = 0): MegaTexture Ask for a base-level page for every image, so the wall fills in without being looked at.
val warmingUp: Boolean True while the base level is not yet complete.
fun frame(command: CommandBuffer, imageIndex: Int) One frame of cache work, recorded into command ahead of the render pass.
fun stats(): MegaTextureStats
override fun close()

MegaTextureCatalog.kt

The stable image-to-index catalog behind a MegaTexture, and the on-disk pyramid cache keyed to it.

MegaTextureCatalog assigns each source file an append-only index and detects files that changed since the last run; MegaTexturePyramidCache stores decoded pages so a later run can skip decoding them.

src/jvmMain/kotlin/afield/graphics/texture/mega/MegaTextureCatalog.kt — read the source

MegaTextureCatalog

class MegaTextureCatalog private constructor(val cacheDir: Path, private val entries: MutableList<Entry>, private val indexByPath: MutableMap<String, Int>, val changedIndices: IntArray)

The set of source images a MegaTexture can show, and the index each one answers to.

Declaration Summary
val cacheDir: Path Where this catalog and its pyramid cache live.
val changedIndices: IntArray Indices whose source file's mtime/size differ from what the catalog recorded — their cached pages describe an older version of the picture and must be discarded.
class Entry(val path: Path, var mtime: Long, var size: Long)
val count: Int
fun path(index: Int): Path?
fun indexOf(path: Path): Int?
fun forEachIndexed(block: (Int, Path) -> Unit) Every (index, path) pair, in index order. Useful for a warm-up pass over the whole set.

MegaTexturePyramidCache

class MegaTexturePyramidCache(val cacheDir: Path, val imageCount: Int, val layout: MegaTextureLayout, val maxLevelCached: Int, val maxVirtualBytes: Long = 32L shl 30) : AutoCloseable

The on-disk store of downsampled pages, one sparse file per level.

Declaration Summary
val cacheDir: Path
val imageCount: Int
val layout: MegaTextureLayout Where page sizes come from. Taken whole so the cache's stride cannot drift from the atlas's.
val maxLevelCached: Int
val maxVirtualBytes: Long
val levels: Int
fun sourceSize(index: Int): Pair<Int, Int>? The source image's pixel size, or null if it has never been decoded.
fun setSourceSize(index: Int, width: Int, height: Int) Record the source image's pixel size, learned from a decode.
fun has(index: Int, level: Int): Boolean True if level level has a page for index on disk.
fun cachedLevels(): List<Int> Which levels this cache stores at all, cheapest first.
fun read(index: Int, level: Int, into: ByteBuffer): Boolean Read the cached page for (image, level) into into, or return false if it is not there. into must have stride bytes remaining; its position is left at the start of the page.
fun write(index: Int, level: Int, page: ByteBuffer) Store the page for (image, level). Silently does nothing for an uncached level.
fun invalidate(indices: IntArray) Forget every cached level for these indices — their source files changed.
fun residentCounts(): IntArray How many pages each cached level holds — the number reported by stats.
override fun close()

MegaTextureDecoder.kt

Background decoding of source images into downsampled MegaTexture pages.

MegaTextureDecoder runs a prioritised worker pool that turns requests into MegaDecodedPages, drawing their pixel memory from per-level pools that the consumer returns with MegaTextureDecoder.recycle.

src/jvmMain/kotlin/afield/graphics/texture/mega/MegaTextureDecoder.kt — read the source

MegaDecodedPage

class MegaDecodedPage(val index: Int, val level: Int, val pageWidth: Int, val pageHeight: Int, val pixels: ByteBuffer, val usedWidth: Int, val usedHeight: Int, val averageColor: Int, val sourceWidth: Int, val sourceHeight: Int, val fromCache: Boolean)

One decoded, downsampled page on its way to the GPU.

Declaration Summary
val index: Int
val level: Int
val pageWidth: Int
val pageHeight: Int
val pixels: ByteBuffer
val usedWidth: Int
val usedHeight: Int
val averageColor: Int Mean colour of the picture, packed 0xAABBGGRR — what a not-yet-resident image draws as.
val sourceWidth: Int
val sourceHeight: Int
val fromCache: Boolean True if this page came off disk rather than out of a JPEG decode.

MegaTextureDecoder

class MegaTextureDecoder(private val catalog: MegaTextureCatalog, private val cache: MegaTexturePyramidCache, private val layout: MegaTextureLayout, threads: Int, private val stillWanted: (index: Int, level: Int, generation: Long) -> Boolean) : AutoCloseable

The parallel decode/downsample/cache queue behind a MegaTexture.

Declaration Summary
val decodedFromCache
val decodedFromSource
val decodeFailures
val cancelled
val inFlight
val queueDepth: Int
val readyCount: Int
fun request(index: Int, level: Int, score: Float, generation: Long) Ask for (image, level). Higher score is served first.
fun poll(): MegaDecodedPage? Take a finished page, or null. The caller owns MegaDecodedPage.pixels until it recycles it.
fun recycle(page: MegaDecodedPage) Return a page's native memory to the pool it came from.
fun trimTo(keep: Int) Drop queued requests beyond keep, cheapest-scoring first.
fun drainAbandoned(sink: (index: Int, level: Int) -> Unit) Hand every abandoned request since the last call to sink, as (index, level).
override fun close()

MegaTextureLayout.kt

The atlas geometry and page-table encoding for a MegaTexture, kept free of any GPU calls.

MegaTextureLayout computes per-level page sizes and slot placement and packs page-table entries; MegaTextureLayoutTest checks that arithmetic without a Vulkan device.

src/jvmMain/kotlin/afield/graphics/texture/mega/MegaTextureLayout.kt — read the source

MegaTextureLayout

class MegaTextureLayout(val imageCount: Int, val levels: Int, val atlasEdge: Int, layersPerLevel: Int, maxBaseLevelLayers: Int, val topPageWidth: Int, val topPageHeight: Int = topPageWidth)

Where every page lives in the atlas, and how a page table entry is packed.

Declaration Summary
val imageCount: Int
val levels: Int
val atlasEdge: Int
val topPageWidth: Int Width of a top-level page. For a film set, the source frame's width exactly.
val topPageHeight: Int Height of a top-level page. Equal to topPageWidth for the square default.
val pageWidths Page width at this level: the top page halved once per level down, never below 1.
val pageHeights
val pagesPerRow Pages across one atlas layer at this level. At least 1, even for a page wider than a layer.
val pagesPerColumn Pages down one atlas layer at this level.
val pagesPerLayer
val layersForLevel
val layerBase First atlas array layer belonging to this level.
val pageCapacity Total pages this level can hold at once.
val identityLevel True when this level can hold every image, so slot == image index.
val totalLayers: Int
fun pageWidth(level: Int): Int
fun pageHeight(level: Int): Int
fun pageBytes(level: Int): Int Bytes one page at this level occupies as RGBA8 — the cache's stride and the upload's size.
val bytesPerLayer: Long Bytes one atlas layer occupies as RGBA8.
val atlasBytes: Long Total atlas allocation in bytes.
fun layerOf(level: Int, slot: Int): Int The atlas array layer holding this level's slot.
fun pageX(level: Int, slot: Int): Int Pixel column of this slot's page within its layer.
fun pageY(level: Int, slot: Int): Int Pixel row of this slot's page within its layer.
fun ceilingFor(wantCounts: IntArray): Int The highest level worth serving, given what the shader asked for and how many pages exist.
fun encode(level: Int, slot: Int, averageColor: Int, usedWidth: Int, usedHeight: Int, fade: Int = FADE_FULL): IntArray One page table entry, as the four uints the shader unpacks.
fun encodeAbsent(averageColor: Int): IntArray An entry for an image with no page yet: mean colour only.
fun decode(entry: IntArray, at: Int = 0): Entry Read an entry back. The inverse of encode, and the Kotlin twin of what the GLSL does, so a test can assert that a page put in a slot comes back out as the same page, in the same layer, with the same aspect, without a GPU.
data class Entry(val resident: Boolean, val pageX: Int, val pageY: Int, val layer: Int, val level: Int, val averageColor: Int, val usedWidth: Int, val usedHeight: Int, val fade: Int = FADE_FULL)
override fun toString(): String

megaTextureShader.kt

GLSL snippets that let a box's own shaders sample a MegaTexture.

megaTextureTemplate returns the slots (declarations, functions, fragment, vertex, all) to interpolate into a shader template; the box writes the geometry and the draw itself.

src/jvmMain/kotlin/afield/graphics/texture/mega/megaTextureShader.kt — read the source

Top-level functions

Declaration Summary
fun megaTextureTemplate(mega: MegaTexture): Map<String, Any> The GLSL a box pulls into its own shaders to talk to a MegaTexture.
fun megaTextureTemplate(layout: MegaTextureLayout, config: MegaTextureConfig): Map<String, Any> megaTextureTemplate from the two objects that actually shape the GLSL.
fun megaVertexSource(layout: MegaTextureLayout, config: MegaTextureConfig): String declarations + functions: everything a vertex stage may legally use.
fun megaAllSource(layout: MegaTextureLayout, config: MegaTextureConfig): String declarations + functions + fragment: the whole thing, for a fragment stage.
fun megaDeclarationsSource(layout: MegaTextureLayout, config: MegaTextureConfig): String The bindings and the numbers that describe the atlas.
fun megaFunctionsSource(layout: MegaTextureLayout, config: MegaTextureConfig): String Everything that only reads the page table, so it is legal in any stage.
fun megaFragmentSource(layout: MegaTextureLayout, config: MegaTextureConfig): String The feedback half: work out what resolution this fragment needs and say so.

textures.kt

Sampled textures for Field 4 shaders: images decoded from disk or memory, CPU-streamed frames, and GPU-writable storage images.

LoadedTexture decodes a PNG/JPEG (via STB) or an EXR (via tinyexr) into a Vulkan image with a mip chain and a default TextureSampler; build one with LoadedTexture.fromFile, LoadedTexture.fromExrFile, LoadedTexture.fromEncodedMemory or LoadedTexture.fromRawRGBA. StreamingTexture re-uploads a CPU ByteBuffer each frame it is asked to, and MappedMonochromeTexture uploads a single-channel image from a shared, header-prefixed mapped buffer only when its frame counter changes. StorageImage is a texture a compute shader writes.

src/jvmMain/kotlin/afield/graphics/texture/textures.kt — read the source

LoadedTexture

open class LoadedTexture protected constructor(private val sourceName: String, val renderer: Renderer = fromContext(), imageFormat: Int = VK_FORMAT_R8G8B8A8_SRGB, decoded: DecodedImage, requestedMipLevels: Int = 1, private val generateMipmaps: Boolean = false, private val mipFilter: Int = VK10.VK_FILTER_LINEAR) : HasImageView, HasDefaultTextureSampler, PreparesSampledImages

Sampled textures for Field 4 shaders: images decoded from disk or memory, CPU-streamed frames, and GPU-writable storage images.

Declaration Summary
val renderer: Renderer The renderer whose device holds the image and whose command pool records the upload.
val fileName: String What the texture was loaded from — a path for a file, or the synthetic name a generated or streamed texture was given. Its last path segment, with the size, is the Vulkan debug label on the image and its view.
var height Height of mip 0, in pixels, taken from the decoded image.
lateinit var image: Image
override lateinit var imageView: ImageView
var mipLevels How many mip levels the image has: the number the decoded source already carried, or else the requested count, which a request of zero or less turns into the full chain for the longest axis.
var width Width of mip 0, in pixels, taken from the decoded image.
var recordedTransition: Boolean Whether the upload and the layout transition into shader-read have been recorded. The recording is gated on it, so the copy happens once however often a frame asks for the texture.
var stgBuffer: VulkanBuffer? The host-visible staging buffer holding the decoded pixels, the whole mip chain one level after another, until the copy into the image is recorded. It stays allocated after that; cleanupStgBuffer releases it.
override var defaultSampler: TextureSampler?
fun setDefaultSampler(anisotropyEnable: Boolean = true, filter: Int = VK10.VK_FILTER_LINEAR, mipmapMode: Int = if (filter == VK10.VK_FILTER_LINEAR) VK10.VK_SAMPLER_MIPMAP_MODE_LINEAR else VK10.VK_SAMPLER_MIPMAP_MODE_NEAREST, addressModeU: Int = VK10.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, addressModeV: Int = VK10.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, addressModeW: Int = VK10.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE): TextureSampler
fun repeatU(anisotropyEnable: Boolean = true, filter: Int = VK10.VK_FILTER_LINEAR, mipmapMode: Int = if (filter == VK10.VK_FILTER_LINEAR) VK10.VK_SAMPLER_MIPMAP_MODE_LINEAR else VK10.VK_SAMPLER_MIPMAP_MODE_NEAREST): TextureSampler
fun cleanup()
fun cleanupStgBuffer()
override fun prepareSampledImagesFor(command: CommandBuffer)
fun recordTextureTransition(cmd: CommandBuffer)
fun transferNow()

StreamingTexture

open class StreamingTexture(val source: ByteBuffer, width: Int, height: Int, renderer: Renderer = fromContext(), imageFormat: Int = VK_FORMAT_R8G8B8A8_SRGB, name: String = "stream_${width}x${height}") : LoadedTexture(name, renderer, imageFormat, DecodedImage(initialSlice(source, width, height, imageFormat), width, height)), Target

Extremely simple streaming texture: call recordUpload every frame.

Declaration Summary
val source: ByteBuffer The buffer each upload reads from, by default: width * height * bytesPerPixel bytes from its current position, which the upload duplicates rather than consumes, so the position is where you left it. Writing into it between uploads is how the texture changes.
override fun render(command: CommandBuffer, index: Int)
fun recordUpload(cmd: CommandBuffer, src: ByteBuffer = source)
fun uploadNow(src: ByteBuffer = source) Convenience for when you don't already have a frame command buffer.

MappedMonochromeTexture

class MappedMonochromeTexture(private val mapped: ByteBuffer, renderer: Renderer = fromContext(), name: String = "mapped_mono", private val headerByteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN, private val headerOffset: Int = 0, private val eagerUpload: Boolean = true, private val requestedMipLevels: Int = 0, private val generateMipmaps: Boolean = true, private val mipFilter: Int = VK10.VK_FILTER_LINEAR) : LoadedTexture(name, renderer, VK_FORMAT_R8_UNORM, decodeInitial(mapped, headerOffset, headerByteOrder)), Target

Streaming texture backed by a mapped shared-memory buffer with a small header: [frame counter (uint64), width (int32), height (int32)] followed by width*height bytes of R8 data.

Declaration Summary
override fun render(command: CommandBuffer, index: Int)
fun recordUploadIfChanged(cmd: CommandBuffer): Boolean Returns true if a new frame was uploaded.
fun uploadNowIfChanged(): Boolean Convenience when you don't already have a frame command buffer.

StorageImage

class StorageImage(val renderer: Renderer, val width: Int, val height: Int, val imageFormat: Int) : HasImageView

Declaration Summary
val renderer: Renderer The renderer whose device allocates the image.
val width: Int Width of the image, in pixels.
val height: Int Height of the image, in pixels.
val imageFormat: Int The image's VK_FORMAT_*, which a shader writing it has to declare to match.
lateinit var image: Image
lateinit override var imageView: ImageView
var recordedTransition Whether the one transition this image needs, from undefined into the general layout a shader reads and writes it in, has been recorded.
fun recordTextureTransition(cmd: CommandBuffer)
fun transitionNow()
fun cleanup()

AudioDecoder.kt

Decodes an audio stream with FFmpeg into signed 16-bit PCM AudioFrames for AudioPlayer.

src/jvmMain/kotlin/afield/graphics/texture/video/AudioDecoder.kt — read the source

AudioDecoder

class AudioDecoder(private val audioPath: String)

AudioDecoder wraps FFmpeg functionality to decode audio streams. Decodes audio to PCM format (signed 16-bit) suitable for OpenAL playback.

Declaration Summary
var audioStreamIndex: Int Index of the stream open decodes, or -1 before a successful open. open takes the first stream the container reports as audio, and fails if there is none.
var sampleRate: Int Sample rate of the source stream in Hz, which need not be the rate of the frames you get: open resamples to the rate it was asked for, and AudioFrame.sampleRate reports that one.
var channels: Int Channel count of the source stream, which is likewise the source's and not the output's: open converts to the count it was asked for, and AudioFrame.channels reports that one.
var duration: Double Length of the audio in seconds, taken from the stream header or, when the stream declares none, from the container's own duration. 0.0 when neither declares one.
fun open(outputSampleRate: Int = 44100, outputChannels: Int = 2): Boolean Open the audio file and initialize decoder.
fun decodeFrame(): AudioFrame? Decode the next audio frame.
fun seek(timestamp: Double): Boolean Seek to a specific timestamp in the audio stream.
fun close() Close the decoder and free resources.

AudioFrame

data class AudioFrame(val data: ByteBuffer, val samples: Int, val sampleRate: Int, val channels: Int, val timestamp: Double)

Data class representing a decoded audio frame in PCM format.

Declaration Summary
val data: ByteBuffer Interleaved PCM samples, signed 16-bit, in native byte order. The decoder owns this buffer and refills it on the next AudioDecoder.decodeFrame, so queue or copy the samples before decoding again.
val samples: Int Samples per channel in data, which therefore holds this many times channels values.
val sampleRate: Int Sample rate of data in Hz, which is the rate asked of AudioDecoder.open rather than the source's own AudioDecoder.sampleRate.
val channels: Int Channel count of data, likewise the count asked of AudioDecoder.open: 1 mono, 2 stereo.
val timestamp: Double When the first sample is heard, in seconds on the source stream's own timeline.

AudioPlayer.kt

Streams PCM audio through OpenAL by queuing a fixed ring of buffers, and reports the playback position that AudioClock uses to keep video in sync.

src/jvmMain/kotlin/afield/graphics/texture/video/AudioPlayer.kt — read the source

AudioPlayer

class AudioPlayer(private val sampleRate: Int = 44100, private val channels: Int = 2)

AudioPlayer manages OpenAL audio playback for synchronized video/audio. Supports streaming audio via buffer queuing for low-latency playback.

Declaration Summary
var volume: Float Gain on the OpenAL source: 0 for silence, 1 for the decoded level, and values outside that range clamped into it. Setting it before initialize is not lost, since initialize applies whatever it holds to the source it creates.
fun initialize(): Boolean Initialize OpenAL context and resources.
fun queueBuffer(audioData: ByteBuffer, numSamples: Int = audioData.remaining() / (channels * 2)): Boolean Queue an audio buffer for playback.
fun queueAudioFrame(audioFrame: AudioFrame): Boolean Queue an AudioFrame for playback (convenience method).
fun processBuffers() Process finished buffers and return them to the free pool. Also updates timing information.
fun play() Start audio playback.
fun pause() Pause audio playback.
fun stop() Stop audio playback and clear all queued buffers.
fun getCurrentTime(): Double Get current playback time in seconds. Tracks total samples played across all buffers for accurate timing.
fun getQueuedBufferCount(): Int Get number of buffers currently queued.
fun getProcessedBufferCount(): Int Get number of buffers processed (ready to be unqueued).
fun isCurrentlyPlaying(): Boolean Check if audio is currently playing.
fun getFreeBufferCount(): Int Get number of free buffers available for queuing.
fun cleanup() Clean up OpenAL resources.

FrameSetBarrier.kt

Holds back presentation of a frame set until every member is ready, so several textures switch frames together.

Ordinary clock ticks cannot replace an unfinished set; a new seek or reconfiguration revision can. FrameSetBarrierTest covers it.

src/jvmMain/kotlin/afield/graphics/texture/video/FrameSetBarrier.kt — read the source

FrameSetBarrier

class FrameSetBarrier<K : Any>

Nonblocking two-phase presentation. Ordinary clock ticks cannot supersede an unfinished set (which would starve a slow decoder); an explicit seek/reconfiguration revision can. Call from one render thread. K must be an immutable source-frame identity.

Declaration Summary
var pending: K? The frame identity select is holding back, or null when nothing is waiting. It survives ordinary clock ticks, and is cleared by presentIfReady, by revise with a new revision, or by reset.
var presented: K? The frame identity the set is showing, or null before the first one. presentIfReady moves pending here once its upload callback has returned, so this names images the caller has committed to rather than ones it has asked for.
fun select(desired: K, revision: Long): K?
fun revise(revision: Long) Cancel stale pending work even when a paused pacer has no replacement key to select.
fun presentIfReady(ready: (K) -> Boolean, upload: (K) -> Unit): Boolean No upload callback runs until the whole set is ready. Commit only after uploads succeed.
fun reset()

FrameSetPacer.kt

Paces an asynchronous frame set so each frame gets a whole source-frame interval to prepare before it is due.

A slow preparation stays the pending demand even when the wall clock has moved past it, which keeps a slow decoder from being starved; seeks and reconfiguration may still replace it. FrameSetBarrierTest exercises it alongside FrameSetBarrier.

src/jvmMain/kotlin/afield/graphics/texture/video/FrameSetPacer.kt — read the source

FrameSetPacer

class FrameSetPacer(private val frameCount: Int)

Gives an asynchronous frame set its whole display interval in which to prepare.

Declaration Summary
data class Demand(val frame: Int, val notBeforeNanos: Long, val generation: Long)
val generation: Long Counter incremented whenever the pacer throws away its pending demand: on a new revision, on a change of cadence, and on reset. Every FrameSetPacer.Demand carries the value current when it was made, so work begun for an older generation can be recognised and dropped.
val pendingFrame: Int? Index of the frame select is currently asking for, or null when nothing is outstanding. markPresented clears it.
fun select(requestedFrame: Int, playing: Boolean, sourceFramesPerSecond: Double, loop: Boolean, revision: Long, nowNanos: Long): Demand?
fun markPresented(frame: Int, nowNanos: Long)
fun reset()

PlaybackClock.kt

Defines PlaybackClock, the time source VideoTexture reads to choose a frame, with two implementations: SystemClock (a monotonic clock with play, pause and seek) and AudioClock (time taken from an AudioPlayer).

src/jvmMain/kotlin/afield/graphics/texture/video/PlaybackClock.kt — read the source

PlaybackClock

interface PlaybackClock

Time source consumed by VideoTexture when selecting the frame for the current update.

Declaration Summary
fun getCurrentTime(): Double Get the current playback time in seconds.
fun isPlaying(): Boolean Check if the clock is currently active/playing.

SystemClock

class SystemClock : PlaybackClock

Monotonic process clock with explicit play, pause, seek, and reset state.

Declaration Summary
override fun getCurrentTime(): Double
override fun isPlaying(): Boolean
fun play()
fun pause()
fun seek(timestamp: Double)
fun reset()

AudioClock

class AudioClock(private val audioPlayer: AudioPlayer) : PlaybackClock

Delegates time and playing state to AudioPlayer's processed-sample and OpenAL-offset clock.

Declaration Summary
override fun getCurrentTime(): Double
override fun isPlaying(): Boolean

VideoDecoder.kt

Decodes video frames with FFmpeg into P010 or RGBA8 buffers (VideoFrame) for VideoTexture.

VideoDecodeFormat picks the output: P010 for ordinary display, RGBA8 when the R/G/B channels are values that 4:2:0 chroma subsampling would destroy. VideoFrameTest covers VideoFrame buffer release.

src/jvmMain/kotlin/afield/graphics/texture/video/VideoDecoder.kt — read the source

VideoDecoder

class VideoDecoder(private val videoPath: String, val outputFormat: VideoDecodeFormat = VideoDecodeFormat.P010) : AutoCloseable

VideoDecoder wraps FFmpeg functionality to decode video frames. Converts decoded frames either to P010 for ordinary video display or to tightly-packed RGBA8 for data-bearing RGB movies. The latter matters for atlases whose R/G/B channels are values rather than colour: routing those through 4:2:0 YUV would irreversibly subsample the channels.

Declaration Summary
val outputFormat: VideoDecodeFormat Pixel layout decodeNextFrame produces: P010 for display video, RGBA8 for a movie whose R/G/B channels carry numbers rather than colour. Fixed at construction, because open configures the FFmpeg scaler for it.
var videoStreamIndex: Int Index of the stream open decodes, or -1 before a successful open. open takes the first stream the container reports as video, and fails if there is none.
var width: Int Frame width in pixels, read from the stream's codec parameters by open, and 0 until then.
var height: Int Frame height in pixels, read from the stream's codec parameters by open, and 0 until then.
var duration: Double Length of the video stream in seconds: its declared duration scaled by its time base, and 0.0 until open reads the header. A container that declares no duration stores a sentinel that is scaled like any other number, so check for a positive value before trusting it.
var fps: Double Average frame rate the stream declares, in frames per second, and 0.0 when the container gives none. VideoTexture falls back to a 0.033 s frame interval rather than dividing by a non-positive rate.
fun open(): Boolean Open the video file and initialize decoder.
fun seek(timestampSeconds: Double): Boolean Seek to a specific timestamp in seconds.
fun decodeNextFrame(): VideoFrame? Decode the next frame in outputFormat. Plane buffers are tightly packed even when FFmpeg's working frame has aligned row padding, so Vulkan uploads never need to know the source stride.
override fun close() Close the decoder and free resources.

VideoDecodeFormat

enum class VideoDecodeFormat

Constants: P010, RGBA8.

VideoFrame

class VideoFrame(val width: Int, val height: Int, val format: VideoDecodeFormat, val plane0: ByteBuffer, val plane1: ByteBuffer, val plane0Stride: Int, val plane1Stride: Int, val timestamp: Double, private val release: () -> Unit = {}) : AutoCloseable

A decoded upload frame. P010 uses both planes; RGBA8 uses plane0 and leaves plane1 empty. The owner must close the frame after upload or when superseding it so its large backing buffers can be reused without waiting for garbage collection.

Declaration Summary
val width: Int Frame width in pixels, the VideoDecoder.width of the decoder that produced it.
val height: Int Frame height in pixels, the VideoDecoder.height of the decoder that produced it.
val format: VideoDecodeFormat Which planes carry data: P010 fills both, RGBA8 fills plane0 and leaves plane1 empty.
val plane0: ByteBuffer Luma samples for a P010 frame, or the whole image for an RGBA8 one. Rows are tightly packed however much padding FFmpeg's working frame carried, so an upload needs no source stride.
val plane1: ByteBuffer Interleaved chroma samples at half the width and half the height for a P010 frame, and an empty buffer for an RGBA8 one.
val plane0Stride: Int Bytes per row of plane0: twice the width for P010 luma, four times for RGBA8. Rows are packed, so this is the row width rather than a padded pitch.
val plane1Stride: Int Bytes per row of plane1, which for P010 is the same as plane0Stride because a chroma row holds half as many samples at twice the size. 0 for an RGBA8 frame.
val timestamp: Double When this frame is shown, in seconds: FFmpeg's best-effort timestamp scaled by the stream's time base. VideoFrameIndex.decoded turns it into a frame number.
val lumaData: ByteBuffer plane0 under its P010 name. For an RGBA8 frame this is the colour image, not luma.
val chromaData: ByteBuffer plane1 under its P010 name, so it is the empty buffer for an RGBA8 frame.
val lumaStride: Int plane0Stride under its P010 name.
val chromaStride: Int plane1Stride under its P010 name, so it is 0 for an RGBA8 frame.
override fun close()

VideoFrameIndex.kt

Frame-number arithmetic for constant-frame-rate video, shared by the code that asks for a frame and the code that reports which frame a decoder produced.

A timeline position belongs to the frame whose interval contains it, so VideoFrameIndex.requested floors seconds * fps. A decoded presentation timestamp marks a frame's own start, which may carry rounding error in either direction, so VideoFrameIndex.decoded rounds to the nearest frame.

src/jvmMain/kotlin/afield/graphics/texture/video/VideoFrameIndex.kt — read the source

VideoFrameIndex

object VideoFrameIndex

CFR frame identity: timeline positions floor; decoded presentation timestamps identify centres.

Declaration Summary
fun requested(seconds: Double, fps: Double): Long The frame shown at timeline position seconds: floor of seconds * fps (tolerating 1e-9 of floating-point noise at a boundary), with negative times clamped to frame 0.
fun decoded(seconds: Double, fps: Double): Long The frame a decoder's presentation timestamp seconds identifies: seconds * fps rounded to the nearest frame.

VideoTexture.kt

Plays a video file into Vulkan textures that a shader can sample, with VideoTexture.load, VideoTexture.play, VideoTexture.pause, VideoTexture.seek and VideoTexture.unload.

Frames come from VideoDecoder as P010 (luma and chroma planes) for display video, or as RGBA8 for movies whose channels carry data. Timing comes from a PlaybackClock: an internal SystemClock by default, or an AudioClock for audio-locked playback.

src/jvmMain/kotlin/afield/graphics/texture/video/VideoTexture.kt — read the source

VideoTexture

class VideoTexture(private val renderer: Renderer = fromContext(), private val clock: PlaybackClock? = null, private val textureSlots: Int = 2, val pixelFormat: VideoDecodeFormat = VideoDecodeFormat.P010, val backgroundDecode: Boolean = false) : HasImageViewArray, afield.graphics.vulkan.Target, HasBrowsable

VideoTexture manages video playback with P010 format support. Maintains configurable texture slot arrays for luma and chroma planes in semi-planar format. Supports load, play, scrub, and unload operations while reusing Vulkan resources when dimensions match.

Declaration Summary
val pixelFormat: VideoDecodeFormat P010 for display video; RGBA8 for movies whose RGB channels carry numeric data.
val backgroundDecode: Boolean Decode and colour-convert on a worker thread, leaving the render callback only the Vulkan upload; with it off, update decodes inline on whichever thread calls it. It also enables prepareFrame, and opens a second decoder so that a short loop does not pay for an end-to-start seek each time around.
fun sampleFrom(source: VideoTexture?)
val sampledFrom: VideoTexture The texture whose images imageViewArray hands out: this one, unless sampleFrom has pointed it at another already-loaded VideoTexture of matching format and size. Only the descriptor array follows the redirection, so getLumaTexture, timestamp and the rest still report this texture's own state.
override fun browsable(): Browsable?
var backgroundDecodeMicros: Long Microseconds the worker's most recent VideoDecoder.decodeNextFrame took, for watching whether decoding is keeping up. It reports the last frame rather than an average, and only backgroundDecode playback writes it.
fun load(videoPath: String): Boolean Load a video file. If dimensions match the previously loaded video, reuses Vulkan resources.
fun play() Start or resume playback.
fun pause() Pause playback.
fun seek(timestampSeconds: Double): Boolean Seek to a specific timestamp in seconds.
fun update(commandBuffer: CommandBuffer): Boolean Update the video texture. Call this every frame to decode and upload new frames when playing. Returns true if a new frame was uploaded.
fun getLumaTexture(): HasImageView? Get the current luma texture.
fun getChromaTexture(): HasImageView? Get the current chroma texture.
override val imageViewArray: List<HasImageView> Get both current textures as an array (luma at index 0, chroma at index 1).
val width: Int Width in pixels of the loaded video, and 0 with nothing loaded. cleanup returns it to 0; unload leaves it, because the Vulkan textures stay allocated for the next video of the same size.
val height: Int Height in pixels of the loaded video, and 0 with nothing loaded, on width's terms.
val duration: Double Length of the loaded video in seconds, read from VideoDecoder.duration, and 0.0 with nothing loaded.
val fps: Double Frame rate the loaded video declares, and 0.0 with nothing loaded. update and prepareFrame use it to turn a clock time into a frame number, and prepareFrame refuses to run while it is 0.
val timestamp: Double Presentation time in seconds of the frame most recently uploaded, which is not the time the clock is asking for. seek moves it to the requested position before any frame has been decoded there.
val playing: Boolean True between play and pause. play does nothing without a loaded video, and running off the end of the video loops back to the start rather than clearing this.
val hasFrame: Boolean True after at least one decoded frame has reached its Vulkan image.
var uploadedFrameIndex: Long Source-frame identity of the image selected for the next draw, not the requested clock.
fun prepareFrame(frameIndex: Long) Prepare a CFR source frame off-thread without uploading it. Do not also schedule update.
fun isFrameReady(frameIndex: Long): Boolean
fun uploadPreparedFrame(commandBuffer: CommandBuffer, frameIndex: Long): Boolean Record this exact image's upload before the same draw as its matching geometry. Never waits.
fun unload() Unload the current video and free resources.
fun cleanup() Clean up all resources including Vulkan textures.
override fun render(command: CommandBuffer, index: Int)

WebcamTexture.kt

Exposes a webcam as an RGBA Vulkan texture, captured through OpenCV.

The first frame is uploaded on open so WebcamTexture.imageView is valid immediately; later frames are grabbed on a background thread and uploaded from the render thread.

src/jvmMain/kotlin/afield/graphics/texture/video/WebcamTexture.kt — read the source

WebcamTexture

class WebcamTexture(private val renderer: Renderer = fromContext(), private val deviceIndex: Int = 0, private val requestedWidth: Int? = null, private val requestedHeight: Int? = null, private val requestedFps: Double? = null, private val startupTimeoutMs: Long = 5_000L) : HasImageView, Target, HasBrowsable, AutoCloseable

Webcam-backed RGBA texture for Vulkan sampling.

Declaration Summary
val width: Int Width in pixels of the frames the camera is actually delivering, which need not be the size this texture asked for. 0 until the camera opens, and 0 again after cleanup; stop leaves it, since the last frame stays in the texture.
val height: Int Height in pixels of the delivered frames, on width's terms. A camera that changes resolution part way through is not followed: those frames are dropped, one warning is logged, and the texture keeps the size it opened at.
val active: Boolean True while the capture thread is grabbing frames: set when the camera opens, cleared by stop and cleanup, and cleared as well if the capture loop throws. A false value after frames had been arriving means capture has stopped, and latestFrameId still names the last frame that got through.
val latestFrameId: Long Monotonic id of the most recently captured frame, or 0 before the first.
fun withLatestFrame(action: (rgba: java.nio.ByteBuffer, width: Int, height: Int, frameId: Long) -> Unit): Boolean Read the newest frame on the CPU, without a GPU round trip.
override val imageView: ImageView
override fun render(command: CommandBuffer, index: Int)
override fun close()
fun start(): WebcamTexture
fun stop()
fun cleanup()
fun update(command: CommandBuffer): Boolean Uploads the newest captured frame, if any. Returns true when a new frame reached the texture.
override fun browsable(): Browsable?

GeometryView.kt

Showing a geometry { } recipe in a studio-lit three view, and handing the recipe language to this document's AI4 chat.

Box.geometry runs a GeometryBuilder block in a box and draws the GeometryScene it builds: every part as a mesh with its own material, the scene's background and ground, and the toolbox's three-quarter camera. Box.animateGeometry re-runs the block with GeometryBuilder.t moving and returns a Frames for the box to launch. The camera is a spec rather than a position, so it survives both a re-run and an animation frame and the model can be orbited while it changes underneath.

geometryAgent bridges those views to AI4. The chat agent is given GEOMETRY_GUIDE before every turn and a geometry.look tool that returns a picture of a view as it stands on screen, which is what lets a model edit a recipe and then see the result. A view that is not open is drawn by the CPU rasterizer instead, without the studio's lighting, and the agent is told which it got.

src/jvmMain/kotlin/afield/graphics/three/GeometryView.kt — read the source

Top-level functions

Declaration Summary
fun ThreeSceneBuilder.geometry(scene: GeometryScene) Draw a GeometryScene in this view: every part as a mesh with its own material, the studio look, the scene's background, and the toolbox's three-quarter camera.
fun Box.geometry(channel: String = "main", name: String = "geometry", height: Int = 460, width: Int? = 680, block: GeometryBuilder.() -> Unit): GeometryScene Run a GeometryBuilder recipe and show the result in a studio-lit three view.
fun Box.animateGeometry(fps: Double = 30.0, channel: String = "main", name: String = "geometry", height: Int = 460, width: Int? = 680, block: GeometryBuilder.() -> Unit): Frames Run a GeometryBuilder recipe fps times a second with GeometryBuilder.t moving, and show each result in the same studio-lit three view: the toolbox's Animate button.
fun Box.geometryAgent() Bridge this document's geometry boxes to AI4: the chat agent learns the GeometryBuilder language (GEOMETRY_GUIDE, before every turn) and gets a geometry.look tool that returns a picture of a geometry view as it is on screen — the toolbox's screenshot-to-code loop, with Field's own chat. /geometry in the chat shows the guide.

Top-level values

Declaration Summary
val GEOMETRY_GUIDE: String The GeometryBuilder language, written for a model: what the madp toolbox's Copy Guide button put on the clipboard. geometryAgent gives it to this document's AI4 chat before every turn.

SimplexView.kt

An orbitable picture of a handful of embeddings and the distances between them, in a box's output.

SimplexView wraps one of the three viewers and draws what the madp tetra page draws: a coloured sphere per point, an edge per pair, the point numbers and edge lengths written onto the picture, and a table of the pairs in the corner. simplexView opens one and hands it back, so SimplexView.show can replace the simplex underneath without resetting the camera you orbited to; showSimplex is the one-call form, for a picture that is drawn once. Before the first simplex arrives the view holds a regular tetrahedron with no lengths on it, and SimplexView.dim greys the current picture while a newer one is being worked out.

What is drawn is an EmbeddingSimplex, which places the vectors in 3D so that the drawn edges are their pairwise distances: exactly so for four points or fewer, a best fit above that. The note in the corner says which of the two you are looking at, and quotes the stress when it is the second.

showSimplex(embeddingSimplex(listOf(clipEncode("cat"), clipEncode("dog"), clipEncode("car"))))

src/jvmMain/kotlin/afield/graphics/three/SimplexView.kt — read the source

Top-level functions

Declaration Summary
fun Box.simplexView(channel: String = "simplex", name: String = "simplex", height: Int = 520, width: Int? = 820, note: String = "waiting for current embeddings"): SimplexView Open a SimplexView in this box's output: an orbitable picture of a few embeddings and the distances between them. It starts as the madp page's empty tetrahedron, saying note.
fun Box.showSimplex(simplex: EmbeddingSimplex, colors: List<String> = SimplexView.SIMPLEX_COLORS, flags: Boolean = false, channel: String = "simplex", name: String = "simplex"): SimplexView Draw simplex in this box's output in one call: simplexView then SimplexView.show. For a picture that changes, keep the SimplexView and call show on it instead, so the view is not reopened (and its camera reset) each time.

SimplexView

class SimplexView internal constructor(private val box: Box, private val uid: String, private val width: Int?, private val height: Int)

An EmbeddingSimplex drawn as the madp tetra page draws it: a coloured sphere per point, an edge per pair, the points' numbers and the edges' lengths written on the picture, a table of the pairs with bars in the corner, and a slow turn until you take hold of it.

Declaration Summary
val view: ThreeGeometryViewState The live three view underneath, for anything this class does not do.
fun show(simplex: EmbeddingSimplex, colors: List<String> = SIMPLEX_COLORS, flags: Boolean = false, table: Boolean = true, note: String? = null) Draw simplex, replacing whatever the view showed.
fun dim(note: String = "diagram out of date") Grey out the picture on screen, for while a newer one is being worked out, with note in the corner. The next show brings it back.

SolidInspection.kt

The three things the solid inspector measures off a part's boundary: which edges are worth drawing, which triangles belong to the same face, and one string that identifies the whole set of parts.

A boundary arrives as a SolidPlanarMesh whose triangles carry the diagonals its triangulator needed and the coplanar edges its Boolean history left behind, so drawing every edge buries the shape under both. solidOutlineEdges keeps an edge unless exactly two triangles meet along it and those two are one flat face: normals parallel and facing the same way, and each triangle's vertices within tolerance of the other's plane. An edge where some other number of triangles meet, or where a triangle is too degenerate to have a normal, stays visible, so an open surface keeps its perimeter rather than reading as a closed solid. The tolerance is the mesh's own nativeTolerance or sixty-four ulps of the part's largest coordinate, whichever is larger, so a part modelled in metres and the same part in millimetres get the same answer.

solidPartTopology groups the triangles into faces by merging across every edge that is not one of those creases. It keeps the canonical double-precision positions and the original triangle order, because a pick is resolved against this copy rather than against the render mesh, whose vertices shading has already split.

solidArtifactDigest hashes part names, positions, indices and faces in sorted name order, so the same boundaries hash the same however the parts were collected. That digest is the geometry identity a review note is pinned to, which is what lets a note outlive a relaunch.

src/jvmMain/kotlin/afield/graphics/three/SolidInspection.kt — read the source

Top-level functions

Declaration Summary
fun solidArtifactDigest(parts: Map<String,SolidPlanarMesh>): String Exact boundary identity, including part names and topology; independent of input map order.

SolidParts.kt

A set of named solid parts, shown in a Three.js scene as something to inspect rather than something to look at. One call inside a three { … } block lays out every part as its own pickable mesh, draws each one's creases and open boundaries over it as dark lines, and installs the inspector: units, the explode slider, per-part visibility, assembly motions and the paths a part came from.

A part arrives as a planar mesh whose triangles carry the diagonals the triangulator needed and the coplanar edges Boolean history left behind, so drawing every edge would bury the shape under both. What is drawn instead is the geometric outline: creases between faces that meet at a real angle, and boundaries where a part ends. Edges between separate parts stay visible, since those are where the assembly is.

Coordinates stay canonical throughout. Positions reach the GPU as Floats relative to one shared render origin, and upAxis = ThreeUpAxis.Z is a rigid display rotation applied on the way out, so neither large coordinates nor a Z-up convention changes the double-precision data the inspector reports or the digest that identifies the artifact.

src/jvmMain/kotlin/afield/graphics/three/SolidParts.kt — read the source

Top-level functions

Declaration Summary
fun ThreeSceneBuilder.solidParts(parts: Map<String,SolidPlanarMesh>, origin: Point3D = Point3D.ORIGIN, assembly: List<ThreeAssemblyMotion> = emptyList(), review: ThreeReviewState? = null, explode: ThreeExplode = ThreeExplode(), units: String = "mm", upAxis: ThreeUpAxis = ThreeUpAxis.Y, partPaths: Map<String,List<String>> = emptyMap(), panel: Int = 0) Draw canonical double boundaries relative to one render origin. Boolean geometry is never read back from these Float buffers. Lines follow geometric creases and open boundaries within each part; coplanar construction boundaries and triangulation diagonals are omitted. Separate parts retain their edges. Z-up is an optional rigid display rotation (x,z,-y); canonical data and artifact identity stay unchanged. An overlay uses the same part ID/group as its solid, so visibility and supplied motions agree. panel is the viewport that draws the parts in a multi-panel scene; -1 draws them in every panel, which is how a sheet of close-ups shows one assembly framed differently in each panel (panel(bounds = …)).

Three.kt

One 3D output for everything Field can draw: Box.three puts meshes and FLines in one orbitable Three.js scene.

Box.three extends Box.threeLines to meshes, and lays several views out side by side when you ask for a contact sheet:

three(channel = "car", height = 460) {
    mesh(car, wireframe = false, pickable = true)
    lines(measurements)                       // annotations in the same space as the solid
    inspect(units = "mm")                     // …and a ruler that reaches both
    camera(yaw = 34.0, pitch = 26.0, distance = 2.4)
    title("ToyCar.glb — 108,936 triangles")
}

With pickable = true a click on a mesh resolves to a vertex, an edge or a triangle (ThreeSurfaceHit), so the measure tool can span a solid and its annotations.

Compared with the CPU rasterizer

The CPU rasterizer in VisionGeometryPreview still draws a mesh to a static PNG, and remains the tool for value-visualizer thumbnails and pixel-exact tests such as MeshDefectRenderTest, since it is deterministic. It cannot orbit, or put a solid and its FLine annotations in one scene.

Shading differs

The shading models differ. The rasterizer is two-light Lambert over vertex colours; this is three.js's MeshStandardMaterial under the viewer's ambient/key/rim rig. The same mesh will not come out pixel-identical. The input is held identical (see toThreeDrawable), as is back-face culling, so a face wound the wrong way is missing from both.

src/jvmMain/kotlin/afield/graphics/three/Three.kt — read the source

Used in: Three.js scenes with three { … }, Geometry from disk, Polytopes: topology first, coordinates later

Top-level functions

Declaration Summary
fun MeshView.toThreeCameraSpec(padding: Double = 1.06): ThreeCameraSpec MeshView's four camera numbers, which ThreeCameraSpec mirrors one for one, plus the viewer's own ThreeCameraSpec.padding, which MeshView has no use for.
fun TriMesh.toThreeBounds(): ThreeBounds This mesh's bounds in the payload's terms.
fun TriMesh.threeFacts(): String The fact line under a rendered mesh — the same sentence afield.geometry.mesh.toFigureHtml computes.
fun Box.three(channel: String = "main", name: String = "three", height: Int = 430, samplesPerCubic: Int = 48, width: Int? = null, block: ThreeSceneBuilder.() -> Unit): ThreeGeometryViewState Display a Three.js viewer built from block, and stream geometry updates to it.
fun Box.three(subject: TriMesh, channel: String = "main", name: String = "three", view: MeshView = MeshView(), height: Int = view.height, title: String? = null, caption: String? = null, facts: Boolean = true, padding: Double = 1.06): ThreeGeometryViewState One mesh, drawn on its own.
fun Box.three(lines: Collection<FLine>, channel: String = "main", name: String = "three", height: Int = 430, samplesPerCubic: Int = 48): ThreeGeometryViewState FLines, drawn on their own — Box.threeLines by another name, for a caller already using three.
fun Box.threeTurntable(mesh: TriMesh, channel: String = "main", name: String = "turntable", height: Int = 260, views: List<MeshView> = MeshView.turntable(4), columns: Int = views.size, title: String? = null, wireframe: Boolean = true): ThreeGeometryViewState One mesh from several angles, in one WebGL context.
fun Box.threeGrid(vararg panels: Pair<String, TriMesh>, channel: String = "main", name: String = "grid", height: Int = 430, columns: Int = 2, view: MeshView = MeshView(), facts: Boolean = false, cellWidth: Int = view.width, sharedScale: Boolean = false, padding: Double = 1.06): ThreeGeometryViewState Several meshes side by side, each in its own panel and each framing its own bounds — unless sharedScale, when every panel frames a box the size of the largest, so a smaller mesh looks smaller. Use that whenever the panels are there to compare sizes.
fun Box.threeGrid(panels: List<ThreeMeshPanel>, channel: String = "main", name: String = "grid", height: Int = 430, columns: Int = 2, view: MeshView = MeshView(), cellWidth: Int = view.width, sharedScale: Boolean = false, padding: Double = 1.06): ThreeGeometryViewState threeGrid with a caller-supplied fact line per cell, and sharedScale as in the other overload.

ThreeSceneBuilder

class ThreeSceneBuilder internal constructor(private val samplesPerCubic: Int)

The receiver of a three { … } block: collects meshes, lines, panels and camera settings for one view.

Declaration Summary
fun helpers(on: Boolean) Whether the reference grid and axis tripod are drawn.
fun mesh(mesh: TriMesh, label: String? = null, wireframe: Boolean = true, cullBackFaces: Boolean = true, flatShading: Boolean = false, opacity: Double = 1.0, panel: Int? = null, id: String = "", onClick: ((ThreeSurfaceHit) -> Unit)? = null, hoverColor: Vec4? = null, pickable: Boolean = false, group: Int = -1, unlit: Boolean = false, depthWrite: Boolean = opacity >= 1.0, explode: FloatArray? = null, solidOpacity: Double? = null, edges: Boolean = false, edgeColor: Vec4? = null, material: ThreeSurfaceMaterial? = null) Add a solid.
fun tools(vararg modules: String) Install optional viewer tools from same-origin ES-module paths. Each module exports a default factory returning a tool descriptor. Modules run with the viewer's privileges; supply only trusted application code. A saved review never imports a module on its own.
fun lines(lines: Collection<FLine>) Add FLines. They keep every behaviour they have in Box.threeLines — picking, hover, fills.
fun lines(vararg lines: FLine) Vararg convenience for lines.
fun camera(yaw: Double = 28.0, pitch: Double = 18.0, distance: Double = 2.7, fieldOfView: Double = 40.0, padding: Double = 1.06, upAxis: ThreeUpAxis = ThreeUpAxis.Y, follow: Boolean = false) Where the camera starts. Setting it turns auto-framing off — you picked an angle, keep it.
fun camera(view: MeshView, padding: Double = 1.06) The camera as a MeshView already describes it — the migration path for existing figures.
fun followCamera(point: afield.math.linalg.Vec3, halfLifeSeconds: Double = .4, epoch: String = "default", horizontalOnly: Boolean = true) Ease along a world point. Half the remaining distance is covered in halfLifeSeconds. Changing epoch translates immediately across a reset; orbit, pan and zoom remain yours. horizontalOnly ignores travel along the camera's up axis, keeping jumps from bobbing the view.
fun studio(ground: Boolean = true, grid: Boolean = true, groundY: Double? = null, groundColor: String = "#08090a", shadows: Boolean = true, exposure: Double = 1.0, ambientOcclusion: Boolean = true) Light the scene like a studio: ACES tone mapping, a shadow-casting key light with a hemisphere fill and a cool rim, ambient occlusion, and a dark ground plane (with a faint grid) that catches the shadows.
fun label(at: Vec3, text: String = "", style: String = "tag", color: String? = null, background: String? = null, border: String? = null, title: String? = null) Pin a piece of text to at: it follows the point as the view turns, and is page text over the canvas rather than a texture, so it stays sharp. It is kept inside the view, sliding along an edge rather than leaving by it, and is hidden behind the camera.
fun overlay(html: String, corner: String = "top-right") Fix an HTML block to a corner of the view — a legend, a table, a key. It does not move with the camera and does not take the pointer, so the view still orbits underneath it. The HTML is inserted as written, as display(...) output is.
fun dimmed(on: Boolean = true) Grey the scene and its labels out: the picture is older than what it shows, and a newer one is on its way. Corner overlays and the note stay as they are.
fun note(text: String) A short line of text in the bottom-left corner of the view: what state the picture is in.
fun hud(on: Boolean) Show or hide the viewer's own controls — X-ray, Export GLB, Pick, the parts list, the triangle count. Off for a view that is a picture to look at, whose corners are its own (overlay, note). Orbiting, zooming and the turntable work either way.
fun background(css: String) Scene background as a CSS colour.
fun linkCamera(group: String = "default") Open the view with Link camera already on, so it orbits, pans and zooms with the other linked views of group in this document — one box per view, or several channels of one box.
fun turntable(degreesPerSecond: Double = 6.0, resumeAfterSeconds: Double = 2.0) Turn the view slowly about its up axis, for a page that is looked at rather than used.
fun shot(periodSeconds: Double = 24.0, keys: List<ThreeShotKey> = ThreeShotSpec.MONEY_SHOT, resumeAfterSeconds: Double = 2.0) Play a looping presentation move: the camera dollies in and out while the model explodes and its surfaces go see-through, on one clock for every panel of the view.
fun panel(title: String? = null, caption: String? = null, facts: String? = null, camera: ThreeCameraSpec? = null, bounds: ThreeBounds? = null, fitBounds: Boolean = false) A titled viewport. Call it once per panel, in order; columns lays them out. A mesh that names no panel goes into the panel declared most recently.
fun columns(n: Int) Panels per row.
fun title(text: String) A single title over a one-panel scene.
fun caption(text: String) A single caption under a one-panel scene.
fun inspect(units: String = "", decimals: Int = 2, explode: ThreeExplode = ThreeExplode(), section: ThreeSection = ThreeSection(), measurements: List<ThreeMeasurement> = emptyList(), pickToMeasure: Boolean = true, groups: List<String> = emptyList(), artifactDigest: String? = null, renderOrigin: DoubleArray = doubleArrayOf(0.0, 0.0, 0.0), renderUpAxis: ThreeUpAxis = ThreeUpAxis.Y, assembly: List<ThreeAssemblyMotion> = emptyList(), review: ThreeReviewState? = null, partPaths: Map<String, List<String>> = emptyMap()) Turn the measure / section / explode toolbar on for this scene.

ThreeMeshPanel

data class ThreeMeshPanel(val label: String, val mesh: TriMesh, val facts: String? = null, val caption: String? = null)

One cell of a threeGrid: a mesh, its heading, and whatever you want written under it.

Declaration Summary
val label: String
val mesh: TriMesh
val facts: String? Written under the panel in monospace. Plain text: it is set as textContent, not as HTML.
val caption: String?

ThreeJsInspector.kt

Measure, section and explode controls for the Three.js viewer, so a 3D preview of something to be built can be inspected without re-running its box.

The inspector answers three questions:

  • how far apart are these two things: ThreeMeasurement, drawn in the scene as a dimension with witness lines and a legible number;
  • what does this look like in section: ThreeSection, a cut plane with a highlighted cut outline, not merely a hidden front half;
  • how does it come apart: ThreeExplode, a parameter with a scrubber, in two modes.

Where the work happens

The displacement, the cut and the closest approach are computed in the viewer (threejs_viewer.html), so moving a slider costs no websocket round-trip and no box re-run. Each of the three is a marked // [viewer-region …] region of that HTML, which ThreeInspectorMeasureDifferentialTest, ThreeInspectorExplodeDifferentialTest and ThreeInspectorSectionDifferentialTest lift out and run on GraalJS against expectations derived from the figure geometry.

The regions cover the geometry only. The three.js plumbing around them (localClippingEnabled, the canvas-texture sprite the dimension text is drawn into, the HUD sliders) is not under those tests, so an empty dimension sprite would leave them green.

src/jvmMain/kotlin/afield/graphics/three/ThreeJsInspector.kt — read the source

Used in: FLine examples (Field graphics), Three.js scenes with three { … }, Point to geometry, keep the context

Top-level functions

Declaration Summary
fun Box.threeInspect(lines: Collection<FLine>, channel: String = "debug", name: String = "inspector", height: Int = 520, samplesPerCubic: Int = 48, autoFrame: Boolean = true, groupBy: (FLine) -> String = { it.label ?: "" }, explode: ThreeExplode = ThreeExplode(), section: ThreeSection = ThreeSection(), measurements: List<ThreeMeasurement> = emptyList(), units: String = "", decimals: Int = 2, pickToMeasure: Boolean = true): ThreeInspectorView A three.js viewer you can ask questions of: measure, section, explode.

ThreeExplode

data class ThreeExplode(val amount: Double = 0.0, val mode: String = "radial", val axis: FloatArray = floatArrayOf(0f, 1f, 0f), val sliderMax: Double = 0.0)

How the viewer pushes a figure's parts apart; amount 0.0 is the assembled state.

Declaration Summary
val amount: Double How far apart to push the groups, in the figure's own units.
val mode: String "radial" — every group slides directly away from the bounds centre. Reads well on an irregular assembly, which is what a routed conduit run is.
val axis: FloatArray
val sliderMax: Double The top of the viewer's own scrubber. 0.0 means "work it out from the bounds", which is what a caller who has not thought about it wants; a caller who has can pin it so that two figures side-by-side scrub at the same rate.
override fun toString(): String

ThreeSection

data class ThreeSection(val enabled: Boolean = false, val normal: FloatArray = floatArrayOf(1f, 0f, 0f), val fraction: Double = 0.5, val outline: Boolean = true, val flip: Boolean = false)

A cut plane through the figure.

Declaration Summary
val enabled: Boolean
val normal: FloatArray
val fraction: Double
val outline: Boolean
val flip: Boolean Which half survives. Flipping is not the same as negating normal: fraction keeps its sense.
override fun toString(): String

ThreeMeasureElement

data class ThreeMeasureElement(val kind: String, val points: List<FloatArray>)

One end of a measurement: a point, a segment or a polygon, in the figure's own coordinates.

Declaration Summary
val kind: String
val points: List<FloatArray>
override fun toString(): String

ThreeMeasurement

data class ThreeMeasurement(val a: ThreeMeasureElement, val b: ThreeMeasureElement, val label: String = "")

A dimension the figure carries from the moment it is drawn, rather than one the user has to pick.

Declaration Summary
val a: ThreeMeasureElement
val b: ThreeMeasureElement
val label: String Prepended to the number. "clear " gives clear 2.5 mm.

ThreeAssemblyMotion

data class ThreeAssemblyMotion(val part: String, val displacement: DoubleArray)

A supplied, independently checked insertion motion, from displaced pose to assembled pose.

ThreeUpAxis

enum class ThreeUpAxis

Canonical up axis; Z-up parts are rotated into the viewer's Y-up display frame.

Constants: Y, Z.

ThreePartTopology

data class ThreePartTopology(val positions: DoubleArray, val indices: IntArray, val edges: List<List<Int>>, val faces: IntArray, @EncodeDefault(EncodeDefault.Mode.NEVER) internal val buffers: Map<String, Int>? = null)

Canonical double coordinates and real boundary edges for artifact-local feature references. Triangle order agrees with the display mesh even when shading duplicates its vertices. faces groups connected coplanar triangles; Boolean provenance edges are not selectable seams.

Declaration Summary
val positions: DoubleArray
val indices: IntArray
val edges: List<List<Int>>
val faces: IntArray

ThreeFeatureSelection

data class ThreeFeatureSelection(val artifactDigest: String, val partId: String, val kind: String, val point: DoubleArray, val triangle: Int? = null, val face: Int? = null, val barycentric: DoubleArray? = null, val vertex: Int? = null, val edge: List<Int>? = null, val edgeT: Double? = null, val region: ThreeRegionSelection? = null)

A selection on one exact artifact. Interpolated points locate a pick, not a machining tolerance.

Declaration Summary
val artifactDigest: String
val partId: String
val kind: String
val point: DoubleArray
val triangle: Int?
val face: Int?
val barycentric: DoubleArray?
val vertex: Int?
val edge: List<Int>?
val edgeT: Double?
val region: ThreeRegionSelection?

ThreeRegionSelection

data class ThreeRegionSelection(val cx: Double, val cy: Double, val width: Double, val height: Double, val angle: Double, val viewport: List<Int>, val pixelCount: Int, val surfaces: List<ThreeRegionSurface>, val view: ThreeReviewState? = null)

A view-space rectangle and the frontmost surface triangles it actually covered.

Declaration Summary
val cx: Double, val cy: Double, val width: Double, val height: Double, val angle: Double
val viewport: List<Int>, val pixelCount: Int, val surfaces: List<ThreeRegionSurface>
val view: ThreeReviewState?

ThreeRegionSurface

data class ThreeRegionSurface(val partId: String, val triangles: List<Int>)

Artifact-local visible triangles belonging to one part in a rectangular review region.

ThreeReviewState

data class ThreeReviewState(val artifactDigest: String, val partIds: List<String>, val renderOrigin: DoubleArray = doubleArrayOf(0.0, 0.0, 0.0), val renderUpAxis: ThreeUpAxis = ThreeUpAxis.Y, val hidden: List<String> = emptyList(), val camera: DoubleArray = doubleArrayOf(), val cameraUp: DoubleArray = doubleArrayOf(0.0, 1.0, 0.0), val cameraFov: Double? = null, val cameraZoom: Double? = null, val target: DoubleArray = doubleArrayOf(), val explode: Double = 0.0, val assemblyStep: Double? = null, val light: DoubleArray = doubleArrayOf(5.0,8.0,6.0), val sectionEnabled: Boolean = false, val sectionFlip: Boolean = false, val sectionFraction: Double = 0.5, val sectionNormal: DoubleArray = doubleArrayOf(1.0,0.0,0.0), val selectedPart: String? = null, val selectedTriangle: Int? = null, val selection: ThreeFeatureSelection? = null)

Saved observations are valid only for the exact artifact digest. Face ordinals are artifact-local.

Declaration Summary
val artifactDigest: String
val partIds: List<String>
val renderOrigin: DoubleArray
val renderUpAxis: ThreeUpAxis
val hidden: List<String>
val camera: DoubleArray
val cameraUp: DoubleArray
val cameraFov: Double?
val cameraZoom: Double?
val target: DoubleArray
val explode: Double
val assemblyStep: Double?
val light: DoubleArray
val sectionEnabled: Boolean
val sectionFlip: Boolean
val sectionFraction: Double
val sectionNormal: DoubleArray
val selectedPart: String?
val selectedTriangle: Int?
val selection: ThreeFeatureSelection?

ThreeInspector

data class ThreeInspector(val enabled: Boolean = true, val groups: List<String> = emptyList(), val explode: ThreeExplode = ThreeExplode(), val section: ThreeSection = ThreeSection(), val measurements: List<ThreeMeasurement> = emptyList(), val units: String = "", val decimals: Int = 2, val pickToMeasure: Boolean = true, @EncodeDefault(EncodeDefault.Mode.NEVER) val artifactDigest: String? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val renderOrigin: DoubleArray = doubleArrayOf(0.0, 0.0, 0.0), @EncodeDefault(EncodeDefault.Mode.NEVER) val renderUpAxis: ThreeUpAxis = ThreeUpAxis.Y, @EncodeDefault(EncodeDefault.Mode.NEVER) val assembly: List<ThreeAssemblyMotion> = emptyList(), @EncodeDefault(EncodeDefault.Mode.NEVER) val review: ThreeReviewState? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val parts: Map<String, ThreePartTopology> = emptyMap(), @EncodeDefault(EncodeDefault.Mode.NEVER) val partPaths: Map<String, List<String>> = emptyMap())

The inspector block of a geometry payload: what the viewer should be doing beyond drawing.

Declaration Summary
val enabled: Boolean
val groups: List<String>
val explode: ThreeExplode
val section: ThreeSection
val measurements: List<ThreeMeasurement>
val units: String Appended to every dimension. The human's panels are in millimetres.
val decimals: Int
val pickToMeasure: Boolean Let the user pick two elements in the viewer and measure between them.
val artifactDigest: String?
val renderOrigin: DoubleArray
val renderUpAxis: ThreeUpAxis
val assembly: List<ThreeAssemblyMotion>
val review: ThreeReviewState?
val parts: Map<String, ThreePartTopology>
val partPaths: Map<String, List<String>> Parts-menu folders, keyed by drawable ID, assembly group name, or review feature ID. Folder labels do not rename geometry or invalidate saved annotations.

ThreeInspectorView

class ThreeInspectorView internal constructor(val state: ThreeGeometryViewState, private val lines: List<FLine>, private val groupKeys: List<String>, private val samplesPerCubic: Int, private val autoFrame: Boolean, private var inspector: ThreeInspector)

A live inspector view: the geometry, plus the three controls, plus the ability to change them.

Declaration Summary
val state: ThreeGeometryViewState
val uid: String
fun inspector(): ThreeInspector The current inspector block, as last published.
fun set(next: ThreeInspector): ThreeInspectorView Republish with a new inspector block. Every mutator below funnels through this.
fun explode(amount: Double): ThreeInspectorView Pull the assembly apart by amount, in the figure's own units. 0.0 reassembles it exactly.
fun explodeAlong(x: Double, y: Double, z: Double, amount: Double): ThreeInspectorView Pull it apart along one direction, layer by layer — the wall build-up view.
fun section(x: Double, y: Double, z: Double, fraction: Double, outline: Boolean = true, flip: Boolean = false): ThreeInspectorView Cut the figure at fraction of the way across its bounds, measured along the given normal.
fun clearSection(): ThreeInspectorView Put the figure back together.
fun measure(a: ThreeMeasureElement, b: ThreeMeasureElement, label: String = ""): ThreeInspectorView Add a dimension between two elements.
fun measureFaces(a: FLine, b: FLine, label: String = ""): ThreeInspectorView Add a dimension between two whole FLines, face to face.
fun clearMeasurements(): ThreeInspectorView
override fun toString(): String

ThreeJsViewer.kt

The Three.js viewer's server side: the geometry payload it is sent, the live state of each view, and Box.threeLines, which puts a viewer in a box's output area.

A view is an iframe on /threejs that orbits, picks and edits the geometry it is given. The payload types (ThreeGeometryPayload, ThreeGeometryData, ThreeAttribute, ThreeIndex, ThreeBounds) mirror three.js BufferGeometry, plus the editable and interactive records a click or a drag reports back. Each view has one ThreeGeometryViewState: call its update to push new geometry to every connected viewer. installThreeJsHandler answers the viewers' websocket messages, and edits, surface clicks and review notes return to the box through it.

src/jvmMain/kotlin/afield/graphics/three/ThreeJsViewer.kt — read the source

Used in: Three.js scenes with three { … }, FLine examples (Field graphics)

Top-level functions

Declaration Summary
fun threeGeometryReviews(uid: String, includeResolved: Boolean = false): kotlinx.serialization.json.JsonObject? Read the live review inbox without opening a second browser or replacing the human selection.
fun detachThreeGlobalListener(session: WebSocketSession) Remove a session from the global-listener set — called from the /ws finally block.
fun installThreeJsHandler()
fun Box.threeLines(lines: Collection<FLine>, channel: String = "debug", name: String = "threejs", height: Int = 420, samplesPerCubic: Int = 48, autoFrame: Boolean = true, editable: Boolean = false, onEditCommitted: ((List<FLine>) -> Unit)? = null, width: Int? = null): ThreeGeometryViewState Display a Three.js viewer for the given lines and stream geometry updates to it.
fun Box.threeLines(vararg lines: FLine, channel: String = "debug", name: String = "threejs", height: Int = 420, samplesPerCubic: Int = 48, autoFrame: Boolean = true, editable: Boolean = false, onEditCommitted: ((List<FLine>) -> Unit)? = null, width: Int? = null): ThreeGeometryViewState Vararg convenience overload of threeLines.
fun Box.threeLines(linesByChannel: Map<String, List<FLine>>, channel: String = "debug", name: String = "threejs", height: Int = 420, samplesPerCubic: Int = 48, autoFrame: Boolean = true, editable: Boolean = false, onEditCommitted: ((List<FLine>) -> Unit)? = null, width: Int? = null): ThreeGeometryViewState Map convenience overload of threeLines, combining all values into one viewer.

ThreeAttribute

data class ThreeAttribute(val itemSize: Int, val array: FloatArray, val type: String = "Float32Array", val normalized: Boolean = false, @EncodeDefault(EncodeDefault.Mode.NEVER) internal val buffer: Int? = null)

Declaration Summary
val itemSize: Int
val array: FloatArray
val type: String
val normalized: Boolean
override fun toString(): String

ThreeIndex

data class ThreeIndex(val type: String, val array: IntArray, @EncodeDefault(EncodeDefault.Mode.NEVER) internal val buffer: Int? = null)

Declaration Summary
val type: String
val array: IntArray
override fun toString(): String

ThreeGeometryData

data class ThreeGeometryData(val attributes: Map<String, ThreeAttribute>, val index: ThreeIndex? = null)

Declaration Summary
val attributes: Map<String, ThreeAttribute>
val index: ThreeIndex?
override fun toString(): String

ThreeBounds

data class ThreeBounds(val min: FloatArray, val max: FloatArray)

Declaration Summary
val min: FloatArray
val max: FloatArray
override fun toString(): String

ThreeMeta

data class ThreeMeta(val lineSegments: Int = 0, val triangles: Int = 0, val points: Int = 0)

Declaration Summary
val lineSegments: Int
val triangles: Int
val points: Int
override fun toString(): String

ThreeEditableNode

data class ThreeEditableNode(val nodeIndex: Int, val kind: String, val to: FloatArray? = null, val c1: FloatArray? = null, val c2: FloatArray? = null)

Declaration Summary
val nodeIndex: Int
val kind: String
val to: FloatArray?
val c1: FloatArray?
val c2: FloatArray?

ThreeEditableLine

data class ThreeEditableLine(val lineIndex: Int, val color: FloatArray, val thickness: Float, val stroked: Boolean, val filled: Boolean, val pointed: Boolean, val pointSize: Float, val callbackId: String = "", val hoverColor: FloatArray? = null, val nodes: List<ThreeEditableNode>, val label: String? = null, val topology: String = "")

Declaration Summary
val lineIndex: Int
val color: FloatArray
val thickness: Float
val stroked: Boolean
val filled: Boolean
val pointed: Boolean
val pointSize: Float
val callbackId: String
val hoverColor: FloatArray?
val nodes: List<ThreeEditableNode>
val label: String? The line's FLine.label, so an editable figure's hits name themselves too.
val topology: String flineTopologyJson for this line — the same string the non-editable path sends.

ThreeEditableScene

data class ThreeEditableScene(val sessionId: String, val lines: List<ThreeEditableLine>)

Declaration Summary
val sessionId: String
val lines: List<ThreeEditableLine>

ThreeInteractiveObject

data class ThreeInteractiveObject(val callbackId: String, val hoverColor: FloatArray? = null, val lines: ThreeGeometryData? = null, val fills: ThreeGeometryData? = null, val points: ThreeGeometryData? = null, val vertices: List<ThreeInteractiveVertex> = emptyList(), val lineIndex: Int = -1, val group: Int = -1, val label: String? = null, val topology: String = "")

Geometry + interaction metadata for a single interactive FLine inside the Three.js viewer.

Declaration Summary
val callbackId: String Server-side callback ID. Empty when this object only has hover (no click).
val hoverColor: FloatArray? Optional hover override colour as [r, g, b, a] each in [0, 1].
val lines: ThreeGeometryData?
val fills: ThreeGeometryData?
val points: ThreeGeometryData?
val vertices: List<ThreeInteractiveVertex>
val lineIndex: Int Index of this FLine among the lines the viewer was given, reported back as lineIndex.
val group: Int Which assembly group this whole object belongs to, or -1 when the payload has no inspector.
val label: String? The line's FLine.label, reported back with a hit so one closure can serve a whole figure.
val topology: String The line's segments and closed subpaths as flineTopologyJson writes them — the same string the interactive SVG puts in data-fline-elements, so the two clickable outputs cannot describe the same line differently. Only their picking differs, which it must: one has a click in a plane, the other a raycast hit that already knows whether it landed on a line, a fill or a point.
override fun toString(): String

ThreeInteractiveVertex

data class ThreeInteractiveVertex(val index: Int, val position: FloatArray)

Declaration Summary
val index: Int
val position: FloatArray

ThreeSurfaceClick

data class ThreeSurfaceClick(val callbackId: String, val id: String = "", val label: String? = null, val drawableIndex: Int = -1, val kind: String = "", val x: Double = 0.0, val y: Double = 0.0, val z: Double = 0.0, val faceIndex: Int = -1, val vertexIndex: Int = -1, val edgeIndex: Int = -1, val u: Double? = null, val v: Double? = null, val w: Double? = null, val ex: Double? = null, val ey: Double? = null, val ez: Double? = null, val frontFacing: Boolean = true, val triangle: List<Double> = emptyList())

A click on a surface drawable, as the viewer posts it — the sibling of FLineClick.

Declaration Summary
val callbackId: String
val id: String ThreeDrawable.id.
val label: String?
val drawableIndex: Int
val kind: String vertex, edge, triangle, or empty for the classifier's refusal.
val x: Double
val y: Double
val z: Double
val faceIndex: Int
val vertexIndex: Int
val edgeIndex: Int
val u: Double?
val v: Double?
val w: Double?
val ex: Double? The point on the named element.
val ey: Double?
val ez: Double?
val frontFacing: Boolean
val triangle: List<Double> The hit triangle's nine ordinates, [ax, ay, az, bx, …], or empty.

ThreeGeometryPayload

data class ThreeGeometryPayload(val type: String = "three/geometry", val uid: String, @EncodeDefault(EncodeDefault.Mode.NEVER) val revision: String? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val sourceBox: String? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val cameraLinkScope: String? = null, val lines: ThreeGeometryData? = null, val fills: ThreeGeometryData? = null, val points: ThreeGeometryData? = null, val bounds: ThreeBounds? = null, val meta: ThreeMeta? = null, val autoFrame: Boolean = true, val interactive: List<ThreeInteractiveObject> = emptyList(), val scene: ThreeEditableScene? = null, val editableEnabled: Boolean = false, val inspector: ThreeInspector? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val drawables: List<ThreeDrawable> = emptyList(), @EncodeDefault(EncodeDefault.Mode.NEVER) val toolModules: List<String> = emptyList(), @EncodeDefault(EncodeDefault.Mode.NEVER) val panels: List<ThreePanel> = emptyList(), @EncodeDefault(EncodeDefault.Mode.NEVER) val columns: Int = 1, @EncodeDefault(EncodeDefault.Mode.NEVER) val background: String? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val helpers: Boolean = true, @EncodeDefault(EncodeDefault.Mode.NEVER) val frameWidth: Int? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val frameHeight: Int? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val camera: ThreeCameraSpec? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val cameraLink: ThreeCameraLinkSpec? = null, @EncodeDefault(EncodeDefault.Mode.NEVER) val cameraFollow: ThreeCameraFollowSpec? = null, @EncodeDefault(EncodeDefault.Mode.NEVER)

Declaration Summary
val type: String
val uid: String
val revision: String?
val sourceBox: String?
val cameraLinkScope: String? Server-owned document identity for optional camera linking; not geometry identity.
val lines: ThreeGeometryData?
val fills: ThreeGeometryData?
val points: ThreeGeometryData?
val bounds: ThreeBounds?
val meta: ThreeMeta?
val autoFrame: Boolean
val interactive: List<ThreeInteractiveObject> Per-FLine interactive objects (separate geometry with click/hover callbacks).
val scene: ThreeEditableScene?
val editableEnabled: Boolean
val inspector: ThreeInspector? Measure / section / explode, when the payload came from Box.threeInspect.
val drawables: List<ThreeDrawable> The scene as ThreeDrawables — geometry plus per-drawable material state.
val toolModules: List<String> Explicit same-origin ES modules installed as optional viewer tools; notes cannot load modules.
val panels: List<ThreePanel> The viewports, when this is a contact sheet. Empty or one-element means a single full-bleed view.
val columns: Int Panels per row, when there is more than one.
val background: String? Scene background as a CSS colour, or null for the viewer's transparent default.
val helpers: Boolean Draw the reference grid and the axis tripod.
val frameWidth: Int? The frame size this figure wants, in CSS pixels, for the viewer to report to its host.
val frameHeight: Int?
val camera: ThreeCameraSpec? Where the camera starts, when the caller picked an angle rather than accepting autoFrame.
val cameraLink: ThreeCameraLinkSpec? Join the document's camera link on load, in this group. See ThreeSceneBuilder.linkCamera.
val cameraFollow: ThreeCameraFollowSpec? Smoothed translation following a world point, independent of orbit and zoom.
val turntable: ThreeTurntableSpec? Turn slowly about the up axis until somebody interacts. See ThreeSceneBuilder.turntable.
val shot: ThreeShotSpec? A looping dolly, explode and x-ray move. See ThreeSceneBuilder.shot.
val studio: ThreeStudioSpec? Tone mapping, soft shadows and a ground. See ThreeSceneBuilder.studio.
val partAppearance: Map<String, ThreePartAppearance> The view's stored part styles, by ThreeDrawable.id, for the viewer to apply.
val labels: List<ThreeLabel> Text pinned to points in the scene. See ThreeSceneBuilder.label.
val overlays: List<ThreeOverlay> HTML blocks in the corners of the view. See ThreeSceneBuilder.overlay.
val dimmed: Boolean Grey the scene out, for a picture that is older than its inputs. See ThreeSceneBuilder.dimmed.
val note: String? A short line in the bottom-left corner. See ThreeSceneBuilder.note.
val hud: Boolean Show the viewer's tool buttons and counts; false for a picture. See ThreeSceneBuilder.hud.
override fun toString(): String

ThreeGeometryViewState

class ThreeGeometryViewState(val uid: String)

Server-side state for a live Three.js geometry viewer.

Declaration Summary
fun reviews(includeResolved: Boolean = false): kotlinx.serialization.json.JsonObject Most recent human selection and unresolved notes. Set includeResolved for completed notes and their history.
fun attach(session: WebSocketSession, buffers: Boolean = false) Attach a websocket session to this viewer and replay the latest payload.
fun update(payload: ThreeGeometryPayload) Push a new payload to all attached viewers.
fun styleParts(expectedRevision: String, styles: Map<String, ThreePartAppearance>, replace: Boolean = false) Persist display styles on this view's source box and send them to connected viewers.
val hasViewer: Boolean Whether a browser viewer is attached, so snapshot has someone to ask.
suspend fun snapshot(timeoutMs: Long = 8000): ByteArray? A PNG of the view as a person sees it — the viewer's own canvas, drawn with its camera where the person left it — or null when no viewer is attached or none answers within timeoutMs.
fun replayTo(session: WebSocketSession) Send the last payload to a newly-attached global listener (replay on subscribe-all).
override fun toString(): String

ThreePartAppearance.kt

Presentation overrides for a named part of a three-dimensional view.

Colours are RGB in 0..1, and a part with no x-ray opacity of its own follows the viewer's slider. None of it changes geometry, picking, assembly groups or review identity: the styles travel beside the geometry rather than baked into it, so restyling a part leaves every note on it attached.

src/jvmMain/kotlin/afield/graphics/three/ThreePartAppearance.kt — read the source

ThreePartAppearance

data class ThreePartAppearance(val color: List<Double>, val unlit: Boolean = false, val xrayOpacity: Double? = null)

One part's overrides, as the viewer receives them.

Declaration Summary
val color: List<Double> Red, green and blue, each a finite value in 0..1; anything else is refused.
val unlit: Boolean Draw the part at its own colour, with neither shading nor tone mapping.
val xrayOpacity: Double? This part's x-ray opacity in 0..1, or null to follow the viewer's slider.

ThreeReview.kt

Geometry identity for a three-dimensional view: what a review note is pinned to.

A note survives anything that is only presentation. Callback and session IDs, hover colour, style and label are left out of the digest, so orbiting, relighting and relaunching identical geometry all leave a note attached. Canonical solids keep their double-boundary identity, and geometry displayed alongside them contributes too: a moving ordinary point invalidates a note even when every solid is unchanged.

src/jvmMain/kotlin/afield/graphics/three/ThreeReview.kt — read the source

ThreeReviewEvidence.kt

Stores the screenshots a geometry review note carries as PNG files beside the document rather than as base64 inside it.

The browser sends a context image and a crop of it, each as a data URL, with the viewport and crop rectangle they were taken from. Kept in the note, those would put megabytes of base64 into a text property that is saved, versioned and diffed on every edit. store writes each image into the document's .geometry-review directory under its own SHA-256 and returns the note's metadata with a /three-review-image URL in place of the bytes, so a second note of the same pixels adds a reference and no file.

The validation is what makes those files safe to serve back. The images have to agree with the note about geometry digest, view and panel; the payload has to be a PNG by signature and by decoding, at the dimensions the browser declared, within 1600 px for the context image and 1024 px for the crop; no image may exceed two million bytes and the directory may not exceed sixty-four million. read answers only for a 64-character hex name that one of the notes it was handed actually cites, so the route cannot be talked into returning a path of the caller's choosing or an image belonging to another view.

src/jvmMain/kotlin/afield/graphics/three/ThreeReviewEvidence.kt — read the source

ThreeScene.kt

The intermediate representation the Three.js viewer draws: a ThreeDrawable is some geometry plus how to draw it.

FLines lower to drawables, TriMeshes lower to drawables, and anything with a toTriMesh() reaches the viewer that way, so a mesh can be orbited and share a scene with its FLine annotations. The viewer knows neither source type; it draws drawables.

The geometry is a ThreeGeometryData, a three.js BufferGeometry of named attributes and an index, and TriMesh's positions, indices, normals and colours transcribe into it directly. What a drawable adds is per-drawable material state, ThreeDrawStyle.

ThreeGeometryPayload's lines, fills and points are unchanged by drawables: a payload with an empty ThreeGeometryPayload.drawables serialises as a lines-only payload always has, which the editable path and the inspector rely on.

src/jvmMain/kotlin/afield/graphics/three/ThreeScene.kt — read the source

Used in: Three.js scenes with three { … }

Top-level functions

Declaration Summary
fun TriMesh.toThreeDrawable(style: ThreeDrawStyle = ThreeDrawStyle.surface(), label: String? = null, panel: Int = 0, group: Int = -1, creaseAngleDegrees: Double = 35.0, id: String = "", callbackId: String = "", hoverColor: FloatArray? = null, pickable: Boolean = false, explode: FloatArray? = null): ThreeDrawable This mesh as something the viewer can draw.
fun TriMesh.outlineEdges(angleDegrees: Double = 1.0, explode: FloatArray? = null): List<Pair<Int, Int>> The edges of this mesh worth drawing: open boundaries and creases, not the diagonals that triangulate a flat face. An edge is kept when it bounds one triangle (or more than two), when a triangle beside it is degenerate, or when its two triangles meet at more than angleDegrees. Returns vertex pairs.
fun TriMesh.toThreeEdgeDrawable(color: FloatArray = floatArrayOf(.08f, .1f, .12f), angleDegrees: Double = 1.0, panel: Int = 0, group: Int = -1, id: String = "", explode: FloatArray? = null): ThreeDrawable outlineEdges as a lines drawable in color ([r, g, b]), drawn over the mesh's own surface (which the viewer pushes back a touch, so they do not z-fight). With explode each line end carries its vertex's offset, so the outline moves with its part.

ThreeDrawStyle

data class ThreeDrawStyle(val kind: String, val doubleSided: Boolean = true, val depthWrite: Boolean = false, val wireframe: Boolean = false, val wireframeColor: FloatArray? = null, val flatShading: Boolean = false, val opacity: Float = 1f, val unlit: Boolean = false, @kotlinx.serialization.EncodeDefault(kotlinx.serialization.EncodeDefault.Mode.NEVER) val xrayOpacity: Float? = null, @kotlinx.serialization.EncodeDefault(kotlinx.serialization.EncodeDefault.Mode.NEVER) val solidOpacity: Float? = null, @kotlinx.serialization.EncodeDefault(kotlinx.serialization.EncodeDefault.Mode.NEVER) val material: ThreeSurfaceMaterial? = null)

How one ThreeDrawable is drawn: which three.js object class, and its material settings.

Declaration Summary
val kind: String "surface", "lines" or "points" — which three.js object class draws this.
val doubleSided: Boolean Draw back faces as well as front ones.
val depthWrite: Boolean Write to the depth buffer.
val wireframe: Boolean Depth-tested wireframe over the shading. Surfaces only; ignored for lines and points.
val wireframeColor: FloatArray? Wireframe colour as [r, g, b, a] in [0, 1], or null for the viewer's default.
val flatShading: Boolean Shade per triangle rather than per vertex: faceted, so a coarse mesh reads as coarse.
val opacity: Float Multiplied into the per-vertex alpha.
val unlit: Boolean Display data colours without lighting modulation.
val xrayOpacity: Float? Optional display opacity multiplier in X-ray mode; null follows the viewer slider.
val solidOpacity: Float? The opacity a ThreeSceneBuilder.shot draws this surface at when the shot's x-ray amount is 0; at 1 it is opacity. null keeps opacity throughout. Lets a see-through view start solid.
val material: ThreeSurfaceMaterial? A physically based material for a surface: base colour, roughness, metalness and emission. null, the default, is the viewer's fixed roughness 0.6 / metalness 0.1 white, which every figure drawn before this existed still gets. afield.geometry.mesh.GeometryBuilder.mat sets it.
override fun toString(): String

ThreeDrawable

data class ThreeDrawable(val geometry: ThreeGeometryData, val style: ThreeDrawStyle, val label: String? = null, val group: Int = -1, val transform: FloatArray? = null, val panel: Int = 0, val callbackId: String = "", val hoverColor: FloatArray? = null, val topology: String = "", val id: String = "", val pickable: Boolean = false)

One thing to draw: geometry, how to draw it, and where it sits.

Declaration Summary
val geometry: ThreeGeometryData
val style: ThreeDrawStyle
val label: String? Reported back with a hit, and shown in the panel chrome when there is one.
val group: Int Assembly group for the inspector's explode, or -1 when this payload has no inspector.
val transform: FloatArray? 16 floats, column-major, as three.js Matrix4.fromArray wants them. null is the identity.
val panel: Int Which viewport in a multi-panel scene draws this. 0 when there is one panel.
val callbackId: String Server-side click callback ID, or empty for a drawable that is not clickable.
val hoverColor: FloatArray? Hover override as [r, g, b, a] in [0, 1].
val topology: String flineTopologyJson, for a drawable that came from an FLine and wants element-level hits.
val id: String A stable name for this drawable, echoed back on every hit as ThreeSurfaceHit.id.
val pickable: Boolean Whether a raycast may hit this drawable at all.
override fun toString(): String

ThreeSurfaceHitKind

enum class ThreeSurfaceHitKind

Which element of a surface a click landed on.

Constants: VERTEX, EDGE, TRIANGLE, UNKNOWN.

ThreeSurfaceHit

data class ThreeSurfaceHit(val id: String = "", val label: String? = null, val drawableIndex: Int = -1, val kind: ThreeSurfaceHitKind = ThreeSurfaceHitKind.UNKNOWN, val position: Vec3, val onElement: Vec3? = null, val faceIndex: Int = -1, val vertexIndex: Int = -1, val edgeIndex: Int = -1, val u: Double = Double.NaN, val v: Double = Double.NaN, val w: Double = Double.NaN, val frontFacing: Boolean = true, val triangle: List<Vec3> = emptyList())

Where a click landed on a solid — the sibling of afield.graphics.render.FLineIntersection.

Declaration Summary
val id: String ThreeDrawable.id, or empty when the author did not name the drawable.
val label: String? ThreeDrawable.label.
val drawableIndex: Int The drawable's ordinal in the payload — the sibling of FLineIntersection.lineIndex.
val kind: ThreeSurfaceHitKind Which part of the triangle was hit: a corner, an edge, the interior, or ThreeSurfaceHitKind.UNKNOWN.
val position: Vec3 Where the ray met the surface, in the scene's own coordinates.
val onElement: Vec3? The point on the named element: the corner for a ThreeSurfaceHitKind.VERTEX, the foot of the perpendicular for an ThreeSurfaceHitKind.EDGE, the hit itself for the interior.
val faceIndex: Int Which triangle of the mesh, counting from zero — the sibling of elementIndex.
val vertexIndex: Int The mesh's own vertex index for a ThreeSurfaceHitKind.VERTEX hit, else -1.
val edgeIndex: Int Which of the triangle's three edges, 0 = (a,b), 1 = (b,c), 2 = (c,a). -1 otherwise.
val u: Double Barycentric weight of the triangle's first corner.
val v: Double Barycentric weight of the second corner.
val w: Double Barycentric weight of the third corner. u + v + w == 1.
val frontFacing: Boolean Whether the ray met the triangle's front face — its winding normal pointing back towards the camera. false means you clicked the inside of the model, which is either a deliberate look through an open shell or the thing that makes a mesh appear to have holes. Nothing in the FLine record corresponds, because an FLine fill is a sheet and has no inside.
val triangle: List<Vec3> The three corners of the hit triangle, in the mesh's winding order.
override fun toString(): String

ThreeCameraSpec

data class ThreeCameraSpec(val yawDegrees: Double = 28.0, val pitchDegrees: Double = 18.0, val distance: Double = 2.7, val fieldOfViewDegrees: Double = 40.0, val padding: Double = 1.06, val upAxis: ThreeUpAxis = ThreeUpAxis.Y, val follow: Boolean = false)

Where the camera stands, in the terms MeshView and VisionGeometryPreview.Camera already use.

Declaration Summary
val yawDegrees: Double Orbit around +y, in degrees. 0 looks straight down -z.
val pitchDegrees: Double Lift above the horizon, in degrees. Positive looks down on the geometry.
val distance: Double Eye distance in multiples of the bounding-sphere radius, as the rasterizer (MeshView) uses it. The Three.js viewer fits the shape to the view instead and ignores it; its framing is padding.
val fieldOfViewDegrees: Double Vertical field of view in degrees.
val padding: Double The Three.js viewer's fit margin around the shape's on-screen outline: 1.0 touches the edges, the default 1.06 leaves a little air. The rasterizer ignores it.
val upAxis: ThreeUpAxis Which world axis is up. ThreeUpAxis.Z keeps the viewer's grid, which lies in the x–y plane, as the floor: yaw turns about +z, pitch lifts toward +z, and yaw 0 looks along +y. The yaw/pitch mapping is the y-up one, rotated rigidly, so ThreeCameraPresetParityTest still covers it.
val follow: Boolean Track the geometry: on every update the camera moves by however far the centre of the bounds moved, keeping whatever orbit and zoom the viewer has, and the grid shifts by whole cells so the floor reads as fixed while the subject crosses it.

ThreeCameraFollowSpec

data class ThreeCameraFollowSpec(val target: List<Double>, val halfLifeSeconds: Double = .4, val epoch: String = "default", val horizontalOnly: Boolean = true)

Follow a world point by translating eye and orbit target together; the human keeps orbit and zoom.

Declaration Summary
val target: List<Double>
val halfLifeSeconds: Double
val epoch: String
val horizontalOnly: Boolean

ThreeCameraLinkSpec

data class ThreeCameraLinkSpec(val group: String = "default")

A view that opens with Link camera already on. Written by ThreeSceneBuilder.linkCamera.

Declaration Summary
val group: String Views link only with views of the same group in the same document.

ThreeTurntableSpec

data class ThreeTurntableSpec(val degreesPerSecond: Double = 6.0, val resumeAfterSeconds: Double = 2.0)

A slow automatic orbit about the view's up axis. Written by ThreeSceneBuilder.turntable.

Declaration Summary
val degreesPerSecond: Double Positive turns the model counter-clockwise seen from above.
val resumeAfterSeconds: Double How long the turn stays paused after the last interaction.

ThreeSurfaceMaterial

data class ThreeSurfaceMaterial(val color: FloatArray = floatArrayOf(1f, 1f, 1f), val roughness: Float = 0.68f, val metalness: Float = 0.04f, val emissive: FloatArray = floatArrayOf(0f, 0f, 0f), val emissiveIntensity: Float = 0f)

A surface's MeshStandardMaterial settings, sent with a ThreeDrawStyle.

Declaration Summary
val color: FloatArray
val roughness: Float
val metalness: Float
val emissive: FloatArray
val emissiveIntensity: Float
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

ThreeStudioSpec

data class ThreeStudioSpec(val ground: Boolean = true, val grid: Boolean = true, val groundY: Double? = null, val groundColor: String = "#08090a", val shadows: Boolean = true, val exposure: Double = 1.0, val ambientOcclusion: Boolean = true)

The studio look: tone mapping, soft shadows, a dark ground and a faint grid, for a scene that is a sculpture on a plinth rather than a technical figure. See ThreeSceneBuilder.studio.

Declaration Summary
val ground: Boolean Draw a ground plane that receives the shadows.
val grid: Boolean Draw a faint grid on the ground.
val groundY: Double? Height of the ground, or null for a little below the scene's lowest point.
val groundColor: String The ground's colour as a CSS colour.
val shadows: Boolean Cast soft shadows from the key light onto the ground and the model itself.
val exposure: Double ACES filmic tone mapping exposure.
val ambientOcclusion: Boolean Screen-space ambient occlusion: contact shadows in creases and under overhangs. Single views only.

ThreeLabel

data class ThreeLabel(val position: List<Double>, val text: String = "", val style: String = "tag", val color: String? = null, val background: String? = null, val border: String? = null, val title: String? = null)

A piece of text pinned to a point in the scene: it follows the point as the view orbits, and is drawn over the canvas as ordinary page text, so it stays sharp at any size. See ThreeSceneBuilder.label.

Declaration Summary
val position: List<Double> The anchor, in scene coordinates: three numbers.
val text: String What the label says; plain text.
val style: String "chip" a boxed label (a point's name), "tag" small dim text (a length on an edge), or "diamond" a small filled marker with no text, coloured by color.
val color: String? Text colour, or for a "diamond" its fill, as a CSS colour; null keeps the style's own.
val background: String? Background as a CSS colour; null keeps the style's own.
val border: String? Border colour as a CSS colour; null keeps the style's own.
val title: String? A tooltip; plain text.

ThreeOverlay

data class ThreeOverlay(val html: String, val corner: String = "top-right")

An HTML block fixed to one corner of the view. See ThreeSceneBuilder.overlay.

Declaration Summary
val html: String The block's HTML. It is inserted as is, so it is trusted as display(...) output is.
val corner: String "top-right", "top-left", "bottom-right" or "bottom-left".

ThreeShotKey

data class ThreeShotKey(val at: Double, val distance: Double = 1.0, val explode: Double = 0.0, val xray: Double = 1.0)

One moment of a ThreeShotSpec: where in the loop it is, and how the view looks there. Between two keys every value eases (smoothstep) from one to the other.

Declaration Summary
val at: Double Where in the loop, from 0 (its start) to 1 (its end, which should look like the start).
val distance: Double Camera distance as a multiple of the distance the view would otherwise stand at.
val explode: Double How far the model is exploded, as a multiple of each mesh's own explode offsets.
val xray: Double How far surfaces with a solid opacity are x-rayed: 0 draws them solid, 1 at their own opacity.

ThreeShotSpec

data class ThreeShotSpec(val periodSeconds: Double = 24.0, val keys: List<ThreeShotKey> = MONEY_SHOT, val resumeAfterSeconds: Double = 2.0)

A looping camera and model move for presentation. Written by ThreeSceneBuilder.shot.

Declaration Summary
val periodSeconds: Double How long one loop takes.
val keys: List<ThreeShotKey> The loop, first key at 0 and last at 1.
val resumeAfterSeconds: Double How long the loop stays held after the last press, wheel, key or drag in the view.

ThreePanel

data class ThreePanel(val camera: ThreeCameraSpec? = null, val title: String? = null, val caption: String? = null, val facts: String? = null, val bounds: ThreeBounds? = null, val fitBounds: Boolean = false)

One viewport in a multi-panel scene, with its own camera and its own caption.

Declaration Summary
val camera: ThreeCameraSpec?
val title: String?
val caption: String?
val facts: String? The computed fact line — counts, extent, topology. Rendered in monospace under the panel.
val bounds: ThreeBounds? What this panel frames, when it differs from the scene's overall bounds.
val fitBounds: Boolean Frame bounds itself rather than the panel's geometry. The viewer normally fits the camera to the mesh's own points, which is what a contact sheet wants; a size comparison wants every panel framed at one scale, so threeGrid(sharedScale = true) sets this.

ThreeWire.kt

What a Three.js view puts on the wire, and what it refuses to.

A view's geometry goes out in two parts. The websocket carries a description: the payload with every numeric array replaced by a reference into a buffer table, so the message stays small however large the scene is. The arrays themselves are one binary response from GET /three-buffers?uid=…&generation=…, written straight out of the payload's own arrays, and only when a viewer asks for them. Nothing is formatted as decimal text, nothing the size of the scene is allocated, and the bytes go out on a Ktor IO thread rather than on the runloop. ThreeWireBuffers is the table a description carries and ThreeWireBuffer one entry in it.

A geometry over AFIELD_THREE_PAYLOAD_LIMIT_MB, 512 MB by default, is not sent at all: ThreePayloadTooLargeException names the largest contributors and the view keeps what it was already showing. That ceiling is for the browser, which has to hold the buffers and then copy them to the GPU; the server streams arrays it is holding anyway.

A client that does not ask for buffers in its hello, as the native visionOS viewer does not, is sent the FLine half of the payload as JSON numbers instead, the format it decodes, without the drawables and solid topology it never reads. That form is encoded only while such a client is connected. The viewer side of all this is the wire-payload region of threejs_viewer.html, and ThreeGeometryWireTest holds the two halves against each other.

src/jvmMain/kotlin/afield/graphics/three/ThreeWire.kt — read the source

ThreePayloadTooLargeException

class ThreePayloadTooLargeException internal constructor(val uid: String, val bytes: Long, val limitBytes: Long, val largest: List<Pair<String, Long>>, message: String) : IllegalStateException(message)

A Three.js view's geometry too large to send.

Declaration Summary
val uid: String
val bytes: Long The geometry's size in bytes, estimated before encoding or measured after.
val limitBytes: Long
val largest: List<Pair<String, Long>> The biggest contributors, largest first, as (what, bytes).

ThreeWireBuffers

data class ThreeWireBuffers internal constructor(val generation: String, val bytes: Long, val entries: List<ThreeWireBuffer>)

Where a description's buffers are: fetch generation from /three-buffers, bytes long, and read each entry as ThreeWireBuffer.count little-endian elements of its type at its offset.

ThreeWireBuffer

data class ThreeWireBuffer internal constructor(val type: String, val offset: Long, val count: Int)

One typed array in a buffer response. Offsets are multiples of eight, so a Float64 view needs no copy.

VisionGeometryPreview.kt

See a Live Metal box's geometry { … } on your desk, without a headset.

What this proves, and what it does not

A // {{ visionmetal }} box has three parts, and only one of them can be checked away from the device today. This previews the geometry part, and only that:

  • The mesh is built by the real builder. capture resolves the box source through the real KotlinJsHeadProfiles.visionMetal profile, compiles it with the real KotlinToJsCompiler, and runs the emitted JavaScript under node against hostShim — a mirror of LiveMetalJavaScript.swift. What gets captured is the byte-identical packed array the headset's Metal vertex buffer would receive. Nothing here reimplements MeshBuilder; if the shared VisionMetalKotlinJs builder source changes, this changes with it.
  • The shader is a stand-in. The box's MSL is not compiled, not translated and not simulated. render shades with a fixed two-light lambert over the vertex colours the builder wrote. So a preview answers "is my geometry the shape I meant?" — the other half of the question the Vision Geometry: Built-in Cube command answers. It cannot tell you your shader is right.

That boundary is repeated in the caption burnt into every rendered frame.

Typical use

val capture = VisionGeometryPreview.capture(box.code)
val png = VisionGeometryPreview.renderPng(capture.mesh())
display(png)

or, from a Live Metal box's own commands, the Preview Vision Geometry action.

src/jvmMain/kotlin/afield/graphics/visionos/VisionGeometryPreview.kt — read the source

VisionGeometryPreview

object VisionGeometryPreview

Declaration Summary
class CapturedMesh(val packed: FloatArray, val indices: IntArray, val label: String = "mesh") One __fieldSetGeometry upload, exactly as the head received it.
data class CapturedUniform(val name: String, val x: Double, val y: Double, val z: Double, val w: Double) One uniform(name, …) write seen by the shim, in order.
class Capture(val meshes: List<CapturedMesh>, val uniforms: List<CapturedUniform>, val log: List<String>, val javascriptChars: Int, val installedFrameCallback: Boolean) Everything one headless run of a Live Metal box's script produced.
data class FrameTick(val time: Double, val deltaTime: Double = 1.0 / 90.0, val frameNumber: Int = 0) How the preview drives onFrame after onStart has run.
fun ticks(frameCount: Int, duration: Double): List<FrameTick> frameCount evenly spaced ticks covering [0, duration), the usual animation sweep.
class NodeUnavailable(message: String) : IllegalStateException(message)
fun capture(source: String, moduleName: String = "vision_geometry_preview", frames: List<FrameTick> = emptyList(), timeoutSeconds: Long = 60): Capture Compile source the way a live box does and run it under node, capturing every geometry upload.
data class Camera(val yawDegrees: Double = 26.0, val pitchDegrees: Double = 16.0, val distance: Double = 2.7, val fieldOfViewDegrees: Double = 42.0) Where the desk-side camera stands. yawDegrees orbits around +y, pitchDegrees lifts above the horizon, and distance is in multiples of the mesh's bounding-sphere radius — so the same camera frames a 2cm mesh and a 2m one.
data class Style(val width: Int = 720, val height: Int = 540, val supersample: Int = 2, val wireframe: Boolean = true, val cullBackFaces: Boolean = true, val background: Int = 0xFF101318.toInt(), val caption: String? = null, val title: String? = null) How the preview draws.
class Raster(val pixels: IntArray, val width: Int, val height: Int, val drawn: Int, val culled: Int, val depth: DoubleArray = DoubleArray(0), val sampleWidth: Int = width, val sampleHeight: Int = height) A finished raster: ARGB pixels, no java.awt anywhere near it.
class ViewFrame internal constructor(bounds: DoubleArray, val camera: Camera, style: Style) Where the eye stands, and how a point in front of it lands on a pixel.
fun viewFrame(mesh: CapturedMesh, camera: Camera = Camera(), style: Style = Style()): ViewFrame The ViewFrame renderPixels would use for this mesh, camera and style.
fun renderPixels(mesh: CapturedMesh, camera: Camera = Camera(), style: Style = Style()): Raster Rasterise mesh: perspective projection, a z-buffer, perspective-correct attributes, a near-plane clip, a depth-tested wireframe and a stand-in shader. No AWT — see Raster.
fun renderPng(mesh: CapturedMesh, camera: Camera = Camera(), style: Style = Style()): ByteArray renderPixels, encoded straight to PNG bytes with afield.media.image.util.PngEncoder.
fun renderFigureHtml(mesh: CapturedMesh, camera: Camera = Camera(), style: Style = Style(), title: String? = style.title): String A complete <figure> — the mesh as an inline PNG, its measurements, and the honesty note as a caption — for display(…) in a box output.
fun render(mesh: CapturedMesh, camera: Camera = Camera(), style: Style = Style()): java.awt.image.BufferedImage renderPixels as an AWT BufferedImage, with the title, stats and caption drawn on.
fun renderContactSheet(mesh: CapturedMesh, cameras: List<Camera> = Camera.turntable(4), style: Style = Style()): java.awt.image.BufferedImage A contact sheet of the same mesh from cameras, side by side with a shared caption.
const val DEFAULT_CAPTION: String The honesty note. Burnt into the image so it cannot be separated from the picture.

VisionGeometryPreviewBox.kt

The in-Field half of VisionGeometryPreview: turning "my mesh only exists in the headset" into a picture in the box you wrote it in.

A Live Metal box's Kotlin never runs on the Mac — it compiles to JavaScript for the head — so its output panel is normally empty of anything but log lines. Box.previewVisionGeometry fills that gap: it takes the box's own source, runs it for real (see VisionGeometryPreview.capture) and draws the resulting mesh straight into the box's output. One command, and your headset geometry is on the desktop.

src/jvmMain/kotlin/afield/graphics/visionos/VisionGeometryPreviewBox.kt — read the source

Top-level functions

Declaration Summary
fun Box.previewVisionGeometry(source: String? = null, camera: VisionGeometryPreview.Camera = VisionGeometryPreview.Camera(), style: VisionGeometryPreview.Style = VisionGeometryPreview.Style(), frames: List<VisionGeometryPreview.FrameTick> = emptyList(), channel: String = "output"): VisionGeometryPreview.Capture? Build this box's geometry { … } for real and draw it into the box's output panel.
fun VisionGeometryPreview.CapturedMesh.toFLines(): List<FLine> The mesh's unique edges as FLines, in metres, for Field's own 3-D viewer.

VisionMetal.kt

Live Metal: the channel that pushes a box's Metal program down to a visionOS head, which compiles and renders it there.

A // {{ visionmetal }} box is sent as a VisionMetalProgramPayload carrying any of four independently revisioned parts — shader (VisionMetalShaderPart), script (VisionMetalScriptPart), uniform names (VisionMetalUniformsPart) and geometry (VisionMetalGeometryPart) — so reloading one part leaves the others resident on the head. VisionMetalProgramState holds one program's current parts on this side; Box.visionMetalProgramOrCreate finds or makes the one for a box, and installVisionMetalHandler registers the /ws handler that heads subscribe through and report VisionMetalStatus back on.

Offline checks of the same program live in VisionMetalProgramCheck (uniform names) and VisionGeometryPreview (the geometry part).

src/jvmMain/kotlin/afield/graphics/visionos/VisionMetal.kt — read the source

Top-level functions

Declaration Summary
fun visionMetalConnectedHeadCount(): Int Number of native Live Metal subscribers currently attached to the replayable program channel.
fun detachVisionMetalGlobalListener(session: WebSocketSession)
fun installVisionMetalHandler()
fun Box.visionMetalUID(name: String = "live-metal"): String The uid the named program of this box publishes under.
fun Box.visionMetalProgram(name: String = "live-metal"): VisionMetalProgramState? The resident program for this box, or null if it has never published one.
fun Box.visionMetalProgramOrCreate(name: String = "live-metal"): VisionMetalProgramState The resident program for this box, created on first use.
fun visionMetalProgramByUID(uid: String): VisionMetalProgramState? Any resident program by its wire uid (<boxId>_<name>), without needing the Box.
fun Box.visionMetalShaderSource(name: String = "live-metal"): String? The MSL this box means, most specific first: its own msl property, then the deprecated use of afield.graphics.shader.fragment that the proof-of-concept relied on, then whatever the head already has.
fun Box.installVisionMetalActions(name: String = "live-metal") Explicit reload commands for a box with a live Metal program.
fun Box.reloadVisionMetalShader(name: String = "live-metal"): Boolean Recompile this box's MSL on the head. Script, geometry and clock survive.
fun Box.reloadVisionMetalUniforms(name: String = "live-metal", uniforms: List<String>? = null): Boolean Redeclare the uniform slot order, regenerating the shader's u_NAME defines.
fun Box.reloadVisionMetalProgram(name: String = "live-metal"): Boolean Republish every resident part, re-reading MSL from the box.
fun Box.visionMetal(shader: String, script: String = "", uniforms: List<String> = emptyList(), vertices: Collection<VisionMetalVertex> = emptyList(), indices: IntArray = intArrayOf(), name: String = "live-metal", launchable: Boolean = true, geometry: VisionMetalGeometryOrigin = if (vertices.isNotEmpty()) VisionMetalGeometryOrigin.WIRE else VisionMetalGeometryOrigin.SCRIPT): VisionMetalProgramState Publish a live MSL + Kotlin/JS program to the visionOS native renderer.

Top-level values

Declaration Summary
const val VISION_METAL_VERSION Wire version 2 splits the single monolithic program payload into four independently revisioned parts — shader, script, uniforms, geometry — so that reloading one does not disturb the others. Version 1 restarted the JavaScriptCore context, the geometry buffers and the frame clock on every shader edit; a v2 shader reload keeps all three resident.
const val VISION_METAL_VERTEX_FLOATS Floats per wire vertex: four float4s — position, normal, colour, then uv plus two free per-vertex channels. This is exactly MemoryLayout<LiveMetalVertex>.stride / 4, so the native head copies the wire array straight into its vertex buffer.
const val VISION_METAL_MAX_UNIFORMS Metal binds one argument-table slot per named uniform; the ABI reserves sixteen float4s.
val visionMetalWireJson The wire encoder. encodeDefaults puts channel/type/version on every message, and spells absent parts as explicit nulls, which is how the head tells "unchanged" from "cleared". Public so tests can assert against the real bytes rather than a lookalike configuration.
var Box.msl: String? Metal Shading Language source for a native visionOS head: both entry points (field_vertex and field_fragment) in one property, since Metal compiles them from one translation unit.
val visionMetalExampleShader A complete hot-reloadable shader for the head's built-in cube; a live-coding starting point.
val visionMetalExampleScript The raw-JavaScript companion to visionMetalExampleShader, for the JVM-side Box.visionMetal entry point. It only writes uniforms, so pair it with geometry = VisionMetalGeometryOrigin.BUILTIN (or with vertices/indices) to give the shader something to draw. Kotlin/JS boxes should use visionMetalExampleKotlinScript instead.
val visionMetalExampleKotlinScript The Kotlin/JS companion to visionMetalExampleShader: builds its own geometry in the head, then drives u_phase from the compositor clock. This is the shape a // {{ visionmetal }} box takes.

VisionMetalVertex

data class VisionMetalVertex(val x: Float, val y: Float, val z: Float, val nx: Float = 0f, val ny: Float = 0f, val nz: Float = 1f, val r: Float = 1f, val g: Float = 1f, val b: Float = 1f, val a: Float = 1f, val u: Float = 0f, val v: Float = 0f, val extra0: Float = 0f, val extra1: Float = 0f)

One authored vertex. position.w and normal.w are supplied by the packer, so the fourteen authored floats become the sixteen the head expects.

Declaration Summary
val x: Float, val y: Float, val z: Float
val nx: Float
val r: Float
val u: Float
val extra0: Float

VisionMetalGeometryOrigin

enum class VisionMetalGeometryOrigin

Who owns the vertex buffer currently resident in the head.

VisionMetalShaderPart

data class VisionMetalShaderPart(val revision: Int, val source: String)

Whatever the box's Kotlin/JS geometry { … } builder last uploaded across the JS bridge.

VisionMetalScriptPart

data class VisionMetalScriptPart(val revision: Int, val source: String, val launchable: Boolean)

VisionMetalUniformsPart

data class VisionMetalUniformsPart(val revision: Int, val names: List<String>)

Declared slot order. The head turns these into #define u_NAME frame.user[i](workspace.md#stdlib-kt--i) for the shader.

VisionMetalGeometryPart

data class VisionMetalGeometryPart(val revision: Int, val origin: VisionMetalGeometryOrigin, val vertices: FloatArray = floatArrayOf(), val indices: IntArray = intArrayOf())

Declaration Summary
val revision: Int
val origin: VisionMetalGeometryOrigin
val vertices: FloatArray
val indices: IntArray
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

VisionMetalProgramPayload

data class VisionMetalProgramPayload(val channel: String = VISION_METAL_CHANNEL, val type: String = "visionmetal/program", val version: Int = VISION_METAL_VERSION, val uid: String, val revision: Int, val shader: VisionMetalShaderPart? = null, val script: VisionMetalScriptPart? = null, val uniforms: VisionMetalUniformsPart? = null, val geometry: VisionMetalGeometryPart? = null)

One program update. Absent parts are unchanged — the head keeps whatever it already has resident. A shader-only reload therefore carries shader and nothing else.

Declaration Summary
val channel: String
val type: String
val version: Int
val uid: String
val revision: Int
val shader: VisionMetalShaderPart?
val script: VisionMetalScriptPart?
val uniforms: VisionMetalUniformsPart?
val geometry: VisionMetalGeometryPart?

VisionMetalStatus

data class VisionMetalStatus(val channel: String, val type: String, val version: Int, val debug_uid: String, val uid: String, val revision: Int, val phase: String, val ok: Boolean, val message: String, val line: Int = 0)

Declaration Summary
val channel: String
val type: String
val version: Int
val debug_uid: String
val uid: String
val revision: Int
val phase: String
val ok: Boolean
val message: String
val line: Int One-based line in the part named by phase, when the head could recover one. Metal compiler diagnostics carry user-relative lines because the head emits #line 1 after its preamble.

VisionMetalProgramState

class VisionMetalProgramState(val uid: String)

A durable program resident in the native visionOS Metal renderer.

Declaration Summary
val currentRevision: Int
var lastStatus: VisionMetalStatus?
val declaredUniforms: List<String> Declared uniform slot order, in the order the shader's u_NAME defines will use.
val shaderSource: String? The MSL currently resident in the head, or null if none has been published.
val scriptSource: String? The compiled Kotlin/JS currently resident in the head, or null.
val scriptIsLaunchable: Boolean Whether the resident script was published in Launch mode.
val geometryOrigin: VisionMetalGeometryOrigin? Who owns the resident vertex buffers.
fun updateShader(source: String): VisionMetalProgramState Recompile the pipeline only. Geometry, uniform slots, the JS context and the clock survive.
fun updateScript(source: String, launchable: Boolean = true): VisionMetalProgramState Reinstall the JavaScriptCore program. This is the one part whose reload restarts the clock.
fun updateUniforms(names: List<String>): VisionMetalProgramState Redeclare the named uniform slots. The head regenerates the shader's u_NAME defines from this list, so it recompiles the pipeline — but leaves script, clock and geometry alone.
fun updateGeometry(vertices: Collection<VisionMetalVertex>, indices: IntArray): VisionMetalProgramState Replace the resident vertex buffers from the JVM side.
fun useBuiltinGeometry(): VisionMetalProgramState Ask for the head's built-in cube explicitly.
fun useScriptGeometry(): VisionMetalProgramState Hand the vertex buffers to the box's Kotlin/JS geometry { … } builder.
fun update(shader: String, script: String = "", uniforms: List<String> = emptyList(), vertices: Collection<VisionMetalVertex> = emptyList(), indices: IntArray = intArrayOf(), launchable: Boolean = true, geometry: VisionMetalGeometryOrigin = if (vertices.isNotEmpty()) VisionMetalGeometryOrigin.WIRE else VisionMetalGeometryOrigin.SCRIPT): VisionMetalProgramState Publish every part at once. Rerunning a Field box takes this path; the individual update* methods are what the explicit reload actions use.
fun onStatus(listener: (VisionMetalStatus) -> Unit): VisionMetalProgramState
override fun toString(): String

VisionMetalKotlinJs.kt

Kotlin/JS declarations for the JavaScriptCore runtime owned by Field4Vision's Live Metal renderer.

Runtime Kotlin/JS compilation currently accepts one source file, so this surface is appended to the user's source rather than compiled as a separate library. Appending means compiler diagnostics keep the user's original line numbers. completionDeclarations mirrors the same public surface in JVM-analysis-safe Kotlin because the completion sidecar does not load the Kotlin/JS compiler frontend.

This only describes values that really exist in LiveMetalJavaScript.swift. Two things cross that bridge: named float4 uniform writes, and whole geometry uploads. Geometry is the head's own — geometry { … } builds an indexed triangle mesh in JavaScriptCore and hands the packed arrays to Metal in one call, so nothing per-vertex crosses the network or the bridge at frame rate.

MSL source and the declared uniform slot order still travel in the visionmetal/program payload: the shader lives in the box's msl property, and slot order comes from the head marker's uniforms= option.

src/jvmMain/kotlin/afield/graphics/visionos/VisionMetalKotlinJs.kt — read the source

VisionMetalKotlinJs

object VisionMetalKotlinJs

Declaration Summary
const val surfaceId: String
val declarations: String
val completionDeclarations: String
fun appendTo(userSource: String): String Add the external surface without changing any user-source line number.

VisionMetalProgramCheck.kt

The offline half of "will this Live Metal program actually run?".

A box names a uniform in three places, and these checks catch them disagreeing on the Mac:

  1. the // {{ visionmetal uniforms=… }} marker, which becomes the head's slot order;
  2. the shader, which reads u_NAME — the head generates #define u_NAME frame.user[i](workspace.md#stdlib-kt--i) from that list, so an undeclared u_NAME is a Metal compile error on the device;
  3. the script, whose uniform("name", …) writes land in a slot only if the name is declared — an undeclared write is dropped without a warning, and the shader just sees zero.

All three failures show up as "it doesn't work in the headset", minutes away from the edit that caused them. Every one is decidable from the box's own text plus a real run of its script, which is what this does. It is the uniform-shaped companion to VisionGeometryPreview.CapturedMesh.windingAgreement.

Nothing here compiles MSL. These are the checks that need only the ABI the head documents, so they run wherever Field runs; the real Metal compile happens on the device.

src/jvmMain/kotlin/afield/graphics/visionos/VisionMetalProgramCheck.kt — read the source

VisionMetalProgramCheck

object VisionMetalProgramCheck

Declaration Summary
data class Problem(val fatal: Boolean, val message: String) One thing that will go wrong, and whether it stops the program dead.
fun problems(code: String, msl: String?, uniformsWritten: Collection<String> = emptyList()): List<Problem> Check a whole box.
fun problems(declared: List<String>, msl: String?, uniformsWritten: Collection<String> = emptyList()): List<Problem> As problems, with the slot order supplied directly.
fun declaredUniforms(code: String): List<String> The marker's uniforms= list, in slot order; empty when the box declares none.
fun summarise(problems: List<Problem>): String? A one-line summary, or null when there is nothing to say.

VisionPose.kt

The pose uplink: head and per-eye viewing geometry travelling from a visionOS head to this Field process, so that the Vulkan renderer on this machine can draw the frame the headset is about to display.

This runs opposite to Field's other visionOS channels. Live Metal (VisionMetalProgramState) pushes a program down to the head and the head renders it; the RTT stream pushes encoded pixels down (native/field-encode/WIRE_FORMAT.md). Neither tells this process where the wearer is looking, so the streamed image can only ever be a world-locked surface — a plane in space. Head-locked, full-field VR needs the pose to come up first, which is what this file receives.

Wire format (__visionpose__, version 1)

JSON over the existing /ws bus, device → here. Three message types:

type when carries
visionpose/hello once per connection device, name, the head's view count and refresh rate
visionpose/frame once per compositor frame the pose, below
visionpose/bye on a clean immersive-space exit device

A visionpose/frame is:

{"channel":"__visionpose__","type":"visionpose/frame","version":1,
 "device":"<uuid>","frameIndex":1842,
 "predictedDisplayTime":123456.789,"sentAt":123456.778,
 "trackingValid":true,
 "originFromDevice":[16 floats],
 "views":[{"transform":[16 floats],"projection":[16 floats]}, …]}

Every matrix is 16 floats, column-major — m[column * 4 + row], which is both simd_float4x4's own storage order on the device and Matrix4f's here, so the array crosses the wire untransposed.

originFromDevice is ARKit's DeviceAnchor.originFromAnchorTransform: the head's pose in the world origin the head established. views[i](workspace.md#stdlib-kt--i).transform is CompositorServices' LayerRenderer.Drawable.View.transform, i.e. device→eye, and views[i](workspace.md#stdlib-kt--i).projection is that drawable's computeProjection(viewIndex:). Both timestamps are in the head's own clock (seconds, LayerRenderer.Clock), so differences within one head are meaningful and comparisons against this machine's clock are not — see VisionPoseFrame.receivedAtNanos.

Not solved here

  • Prediction. predictedDisplayTime is when the head's own next frame displays, which is earlier than when a frame rendered here could arrive. Shifting the query forward by the measured render→encode→network→decode latency is left to the head; this file carries the timestamp unchanged so that shift is a one-line change there.
  • Transport. JSON over the WebSocket reuses the bus, the dev-certificate trust and the reconnect logic the other native clients already have, at the cost of TCP in the tracking path. A pose is ~700 bytes; at 90 Hz that is ~60 kB/s, which is not a reason to move it to UDP; a measured latency would be.

src/jvmMain/kotlin/afield/graphics/visionos/VisionPose.kt — read the source

Used in: Field on Apple Vision Pro

Top-level functions

Declaration Summary
fun installVisionPoseHandler()
fun visionPoseLatest(): VisionPoseFrame? The newest pose from any head, or null if none has ever arrived.
fun visionPoseConnectedHeadCount(): Int Heads currently publishing pose.
fun visionPosePingHeads(): Int Ask every attached head to echo a timestamp, so the two clocks can be related.
fun visionPoseLatencyReport(pings: Int = 8, spacingMillis: Long = 150): String Measure the clocks, then print where the time goes. This is the one call to make on device.

Top-level values

Declaration Summary
const val VISION_POSE_CHANNEL
const val VISION_POSE_VERSION Wire version 1. Bump when the shape changes; a head sending another version is dropped rather than half-read, so an old Field4Vision build reports as silence rather than as garbage poses.
val visionPoseWireJson

VisionPoseWireView

data class VisionPoseWireView(val transform: FloatArray, val projection: FloatArray)

Per-eye viewing geometry exactly as the head's compositor stated it.

Declaration Summary
val transform: FloatArray device→eye, column-major.
val projection: FloatArray The compositor's projection for this eye, column-major.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

VisionPoseWireController

data class VisionPoseWireController(val chirality: String, val tracked: Boolean, val originFromGrip: FloatArray, val select: Boolean = false, val trigger: Float = 0f, val triggerPressed: Boolean = false, val squeeze: Float = 0f, val squeezePressed: Boolean = false, val trackpadX: Float = 0f, val trackpadY: Float = 0f, val trackpadTouched: Boolean = false)

One tracked controller, as the head reports it.

Declaration Summary
val chirality: String "left" | "right" | "unknown" — the head's held chirality, not the device's inherent one.
val tracked: Boolean The accessory is present and its pose is being tracked.
val originFromGrip: FloatArray world→grip, column-major. Grip, not aim: it is the pose a hand-held object should follow.
val select: Boolean
val trigger: Float
val triggerPressed: Boolean
val squeeze: Float
val squeezePressed: Boolean
val trackpadX: Float
val trackpadY: Float
val trackpadTouched: Boolean
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

VisionPoseWireFrame

data class VisionPoseWireFrame(val channel: String = VISION_POSE_CHANNEL, val type: String = "visionpose/frame", val version: Int = VISION_POSE_VERSION, val device: String, val frameIndex: Long, val predictedDisplayTime: Double, val sentAt: Double, val trackingValid: Boolean, val originFromDevice: FloatArray, val views: List<VisionPoseWireView>, val controllers: List<VisionPoseWireController> = emptyList())

One frame of head tracking, as it arrives. VisionPoseFrame is the form Field consumes.

Declaration Summary
val channel: String
val type: String
val version: Int
val device: String
val frameIndex: Long
val predictedDisplayTime: Double
val sentAt: Double
val trackingValid: Boolean
val originFromDevice: FloatArray world→head (ARKit originFromAnchorTransform), column-major.
val views: List<VisionPoseWireView>
val controllers: List<VisionPoseWireController> Tracked controllers as of this pose. Additive and optional: a head that reports none, or an older head that does not know the field, simply yields no controllers. That is why adding this did not need a VISION_POSE_VERSION bump — on a JSON channel with ignoreUnknownKeys, an optional field is compatible in both directions. Bump the version only for a change that would misread an old message.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

VisionPoseHello

data class VisionPoseHello(val channel: String = VISION_POSE_CHANNEL, val type: String = "visionpose/hello", val version: Int = VISION_POSE_VERSION, val device: String, val name: String = "", val viewCount: Int = 2, val refreshHz: Double = 90.0)

A head announcing itself, so a listener can size buffers before the first frame arrives.

Declaration Summary
val channel: String
val type: String
val version: Int
val device: String
val name: String
val viewCount: Int
val refreshHz: Double

VisionEyeView

class VisionEyeView(val viewMatrix: Matrix4f, val projection: Matrix4f, val tanLeft: Float, val tanRight: Float, val tanDown: Float, val tanUp: Float)

One eye, ready to render with.

Declaration Summary
val viewMatrix: Matrix4f
val projection: Matrix4f
val tanLeft: Float
val tanRight: Float
val tanDown: Float
val tanUp: Float
override fun toString(): String

VisionController

class VisionController(val hand: OpenXRHmdTarget.Hand?, val state: OpenXRHmdTarget.ControllerState)

One controller, ready to use.

Declaration Summary
val hand: OpenXRHmdTarget.Hand?
val state: OpenXRHmdTarget.ControllerState
override fun toString(): String

VisionPoseFrame

class VisionPoseFrame(val device: String, val frameIndex: Long, val predictedDisplayTime: Double, val sentAt: Double, val receivedAtNanos: Long, val trackingValid: Boolean, val originFromDevice: Matrix4f, val headPosition: Vector3f, val headOrientation: Quaternionf, val eyes: List<VisionEyeView>, val controllers: List<VisionController> = emptyList())

One arrived pose. Immutable: VisionPoseStream republishes a whole new frame rather than mutating the last one, so a renderer that captured VisionPoseStream.latest at the top of its frame keeps a consistent head and eyes all the way through it — the association between a pose and the image drawn from it is the whole point of the milestone.

Declaration Summary
val device: String
val frameIndex: Long
val predictedDisplayTime: Double Head clock, seconds: when the head expects to display its own next frame.
val sentAt: Double Head clock, seconds: when the head handed this to its socket.
val receivedAtNanos: Long This machine's System.nanoTime on arrival. Not comparable with the two above.
val trackingValid: Boolean
val originFromDevice: Matrix4f world→head.
val headPosition: Vector3f
val headOrientation: Quaternionf
val eyes: List<VisionEyeView>
val controllers: List<VisionController>
fun controller(hand: OpenXRHmdTarget.Hand): VisionController? The controller in hand, or null. Unknown-handed controllers never match.
val sendLatencySeconds: Double Head clock, seconds: how long the head spent between sampling and sending.
override fun toString(): String

VisionPoseStream

object VisionPoseStream

Everything known about the heads currently reporting pose.

Declaration Summary
var latest: VisionPoseFrame?
val heads: MutableMap<String, VisionPoseHello> The most recent visionpose/hello, per device.
var logHz: Double Print a pose summary at this rate. Zero is silent; the first pose of a run always prints one line whatever this says, because "did anything arrive at all" is the question a fresh headset raises and grepping for silence is not an answer.
val frameCount: Long Poses accepted since startup.
val regressedCount: Long Poses dropped for arriving out of order, rather than applied over a newer one.
val rejectedCount: Long Messages on the channel that were not usable: wrong version, malformed, wrong shape.
val intervalMillis: Double Smoothed arrival interval in milliseconds, an EWMA over the gaps between arriving frames. This is the rate the head is delivering, which need not be the rate it advertises.
val connectedHeadCount: Int Heads that have said hello on a still-open session.
fun onPose(listener: (VisionPoseFrame) -> Unit): VisionPoseStream
fun removePoseListener(listener: (VisionPoseFrame) -> Unit)
fun accept(text: String, session: WebSocketSession? = null): Boolean Decode and publish one message from the channel. The handler is a thin wrapper over this, so a test can drive the real path — decode, matrix composition, tangent extraction, statistics — without a socket.
fun detach(session: WebSocketSession) Forget a head whose session has gone, called from the /ws teardown.
fun summary(): String One line for a log, a box output or a smoke test.
fun reset() Forget every pose, statistic, listener and connected head, so a test can start from an empty stream.

VisionPoseLatency.kt

Where the time goes between a head moving and the resulting image reaching it.

This attributes latency to stages rather than reporting one number, because the plausible explanations for "it feels laggy" are spread across five machines' worth of stages, and two of those stages hold frames back in order to do their job: the receiver's jitter buffer and the producer's render fence.

Two clocks, and why the headline number needs neither reconciled

The head's clock (LayerRenderer.Clock) and this process's System.nanoTime are unrelated. That would normally make one-way timings unmeasurable. It does not here, because of a property of the wire we already have: a pose's predictedDisplayTime is in the head's clock, and the media wire carries it back to the head as poseDisplayTimeNanos. So the head can subtract two of its own timestamps and get motion-to-photon with no clock agreement at all. That is why the headline figure is measured on the device and reported here, rather than computed here.

The offset is still wanted, for splitting the remainder into "our side" and "their side". It is estimated NTP-style from PING/PONG round trips, keeping the sample with the smallest round trip rather than an average: the minimum is the one least polluted by queuing in either direction, and averaging RTTs on a contended Wi-Fi link estimates the contention, not the offset.

The two stages that hold frames back

The receiver runs a jitter buffer (RTTStream.bufferFrames) that holds frames so that network jitter does not become dropped frames. It is reported as its own span. Without that split, the buffer doing exactly its job looks like the network being slow, and the obvious "fix" — shrinking it — trades smoothness for a better number.

Likewise SUBMIT_TO_ENCODED includes the render fence: the producer only signals a surface once its GPU work has completed, so scene complexity lands in that span rather than anywhere else.

What this cannot see

The device's last observable moment is the decoded frame reaching the display texture. Actual photons are the compositor's business, up to one refresh later, and with the present path still on RealityKit there is no hook for it. So the headline is a lower bound, short by up to one compositor frame. That closes when the present path moves to Compositor Services, which is also what makes reprojection possible; it is not worth faking before then.

src/jvmMain/kotlin/afield/graphics/visionos/VisionPoseLatency.kt — read the source

VisionPoseLatency

object VisionPoseLatency

Declaration Summary
const val PING
const val PONG
const val REPORT
const val POSE_TRANSIT Spans this process can time on its own clock.
const val POSE_AGE_AT_LATCH
const val LATCH_TO_SUBMIT
const val SUBMIT_TO_ENCODED
var clockOffsetNanos: Long head clock ≈ this clock + clockOffsetNanos. Zero until a pong lands; offsetKnown says which, because a zero offset is also a legitimate estimate and silently treating "unknown" as "aligned" would report host-side spans as if the two machines shared a clock.
var offsetKnown: Boolean
var bestRoundTripNanos: Long Smallest round trip seen, the sample the offset is taken from.
fun record(name: String, nanos: Long)
fun ping(): String Build a ping. Send it to a head; acceptPong closes the loop.
fun acceptPong(hostSendNanos: Long, headRecvNanos: Long, headSendNanos: Long) Close a round trip.
fun headToHostNanos(headNanos: Long): Long Convert a head-clock instant into this process's clock. Only meaningful once offsetKnown.
fun latched(seq: Long, pose: VisionPoseFrame) A stereo target latched pose and is about to render stream frame seq.
fun submitted(seq: Long) The producer has signalled the surface for seq to the encoder.
fun encoded(seq: Long) The helper reported {"ev":"encoded","seq":…}.
var deviceSpans: Map<String, DeviceSpan> Spans the head measured on its own clock, newest report wins.
var deviceReportCount: Long
data class DeviceSpan(val p50Millis: Double, val p95Millis: Double, val maxMillis: Double, val n: Int)
fun acceptDeviceReport(spansByName: Map<String, DeviceSpan>)
fun report(): String The whole chain, host spans then device spans, with the headline last.
fun reset() Clears every span, ping and device report, and the clock-offset estimate with them, so a measurement run starts from nothing. The offset has to be re-acquired after this.
override fun toString(): String

VisionPoseStereoSource.kt

Drives Field's stereo rendering from a visionOS head's uplinked pose — the consumer end of VisionPoseStream.

Attach it to a afield.graphics.vulkan.targets.SideBySideStereoTarget and the eye passes render from the headset rather than from a desktop mock:

val sbs = SideBySideStereoTarget(renderer)
sbs.poseSource = VisionPoseStereoSource()

The projection is rebuilt rather than forwarded. The head sends the matrix its compositor computed, but that carries the compositor's near/far and depth range; Field's Vulkan pipelines use Options.zNear/Options.zFar with a 0..1 depth range. So this takes the frustum tangents — the convention-free part — and builds a projection exactly the way afield.graphics.vulkan.targets.OpenXRHmdTarget builds one from XrFovf's angles. That is the whole reason VisionEyeView carries tangents at all.

The view matrix, by contrast, is forwarded untouched: the wire already carries world→eye, composed on arrival the same way the head composes its own, so there is nothing to reinterpret.

src/jvmMain/kotlin/afield/graphics/visionos/VisionPoseStereoSource.kt — read the source

VisionPoseStereoSource

class VisionPoseStereoSource(private val newest: () -> VisionPoseFrame? = { VisionPoseStream.latest }, var zNear: Float = Options.zNear.toFloat(), var zFar: Float = Options.zFar.toFloat()) : StereoPoseSource

Declaration Summary
var zNear: Float
var zFar: Float
var latched: VisionPoseFrame? The pose the current frame is being rendered from, or null before the first successful latch.
var staleLatchCount: Long Latches that found nothing usable, so the view held its last pose.
override val latchedFrameId: Long
override val latchedDisplayTime: Double
override val eyeCount: Int
override fun latch(): Boolean
override fun eyeViewMatrix(eyeIndex: Int, out: Matrix4f): Matrix4f
override fun eyeProjectionMatrix(eyeIndex: Int, out: Matrix4f): Matrix4f
override fun controller(hand: OpenXRHmdTarget.Hand, out: OpenXRHmdTarget.ControllerState): Boolean
val latchedControllers: List<VisionController> Controllers carried by the latched pose, whichever hand — including unknown-handed ones.
override fun headPose(outPosition: Vec3, outOrientation: Quat): Boolean
override fun toString(): String

CaptureOrientation.kt

The row order a pixel source hands to an encoder, and the paths that are held to it.

Row order is not a property of the encoder, and not a property of the file format: it is a property of the readback. A "flip Y" at a call site says what a line of code does and not what it is for, so each capture path carries the order its source delivers instead, and a test reads the source back to check that the two agree.

src/jvmMain/kotlin/afield/graphics/vulkan/CaptureOrientation.kt — read the source

GpuImageCapture.kt

src/jvmMain/kotlin/afield/graphics/vulkan/GpuImageCapture.kt — read the source

ModuleDestinations.kt

Where a module's drawing ends up, and a picture of it there: the window it is added to, or the render-to-texture that holds it — found by walking what the renderer draws, not by being told.

renderer += module puts a module in a window's main pass; rtt += module in a render target, which draws when something reaches it (usually the renderer's preTargets). Walking the windows' passes, everything their others and preTargets reach, and the headset target finds every place that draws the module, compared by identity. A target drawn by some other path is not found.

ModuleDestinations.capture reads one of those places back as PNG bytes, and ModuleDestinations.renderDisposable draws a module alone into a temporary render target and returns that picture. StackSL's auto-study uses both to show what a module drew.

src/jvmMain/kotlin/afield/graphics/vulkan/ModuleDestinations.kt — read the source

ModuleDestinations

object ModuleDestinations

Finds where a module draws, and captures it there.

Declaration Summary
sealed class Destination A place that draws a module.
fun of(renderer: Renderer, module: Any): List<Destination> Every place renderer draws module — a graphics or mesh module pointer. Compute modules draw nowhere.
fun capture(destination: Destination, timeoutMs: Long = 8000): ByteArray A PNG of destination as it is being drawn: the window's swapchain, or the render target's first colour attachment, both as displayed (sRGB-encoded bytes). Blocks until a frame has drawn it, so call it off the main thread, which draws. Throws with the reason when it cannot: a render target made without colorTransferSrc, a window that does not allow capture, or one that is not drawing.
fun renderDisposable(renderer: Renderer, module: ModulePointer, width: Int = 1024, height: Int = 1024, timeoutMs: Long = 8000): ByteArray A PNG of module drawn once more, by itself, into a render target of our own — the same module, with its own state, meshes and uniforms, so a ping-pong module still samples its real previous frame, and nothing else that draws where it draws is in the picture.

Renderer.kt

The Vulkan renderer: one Renderer owns the instance, device, queues, command pools and the swapchain of each of its windows, and records, submits and presents a frame per tick.

A box beneath the renderer box reaches it as renderer and adds modules with renderer += module, which puts them in the main window's Renderer.renderList; compute modules go to Renderer.computeList. Each window has its own RenderTarget, found with Renderer.renderTargetFor. Renderer.captureWindowToFile writes a window's next frame to a PNG, and Renderer.cleanup shuts the renderer down, releasing its device, windows and targets.

The file also holds Target, the callback shape for drawing into a command buffer, and CPUSpan, the per-span CPU timing statistics the frame loop records.

src/jvmMain/kotlin/afield/graphics/vulkan/Renderer.kt — read the source

Used in: Neural image rendering with OpenDLSS-NR, Profiling

Top-level functions

Declaration Summary
fun target(a: (CommandBuffer, Int) -> Unit)

Target

fun interface Target

Something that records drawing into CommandBuffer for swapchain image index.

CPUSpan

object CPUSpan

Lightweight CPU span stats.

Declaration Summary
data class Stat(val samples: ArrayDeque<Long> = ArrayDeque())
data class Snapshot(val label: String, val n: Int, val avgMs: Double, val minMs: Double, val maxMs: Double, val p1Ms: Double, val p10Ms: Double, val p50Ms: Double, val p90Ms: Double, val p99Ms: Double)
val stats
val lock
val version
val listeners
inline fun time(label: String, block: () -> Unit)
fun notifyListeners(v: Long)
fun addListener(listener: (Long) -> Unit)
fun snapshot(): List<Snapshot>
fun snapshotAndMaybePrint(everyNFrames: Int, frame: Long, header: String = "CPU")
fun version(): Long

Renderer

class Renderer(private val mainWindow: Window, additionalWindows: List<Window> = emptyList())

Declaration Summary
data class VertexUploadFrameStats(val frameId: Long, val recordedBytes: Long, val immediateBytes: Long)
data class WindowContext(val window: Window, val surface: Surface, var swapChain: SwapChain, var depthAttachments: MutableList<Attachment>, var commandBuffers: Array<CommandBuffer>, var computeCommandBuffers: Array<CommandBuffer>, var fences: Array<Fence>, var computeFences: Array<Fence>, var computeFinishedSemaphores: Array<Semaphore>, var numImages: Int, var currentImage: Int = 0, var renderTarget: RenderTarget, var profilerState: ProfilerState? = null, var computeSecondary: MutableMap<String, Array<CommandBuffer>> = mutableMapOf(), var graphicsSecondary: MutableMap<String, Array<CommandBuffer>> = mutableMapOf(), var computePoolsPerThread: MutableMap<Long, CommandPool> = mutableMapOf(), var graphicsPoolsPerThread: MutableMap<Long, CommandPool> = mutableMapOf())
data class ProfilerState(val profiler: GpuProfiler, var markers: Array<FrameMarkers>, var perImageModuleMarks: Array<MutableList<ModuleMarks>>, val pending: ArrayDeque<Pending> = ArrayDeque(), val lock: Any = Any())
val windows: List<Window>
val window: Window
var lastVertexUploadFrameStats: VertexUploadFrameStats
val commandPool: CommandPool
val computeCommandPool: CommandPool
val device: Device
val graphQueue: GraphicsQueue
val instance: Instance
val physicalDevice: PhysicalDevice
val pipelineCache: PipelineCache
val presentQueue: PresentQueue
val surface: Surface
val swapChain: SwapChain
var computeList: ComputeTarget
var currentImage: Int
val numImages: Int
val depthAttachments: MutableList<Attachment>
var renderList: RenderTarget
fun windowContext(index: Int = 0): WindowContext
fun renderTargetFor(windowIndex: Int = 0): RenderTarget
fun windowIndex(window: Window): Int
fun renderTargetFor(window: Window): RenderTarget
var cleanup: MutableList<Runnable> Releases to run once the frames now in flight have retired (see render). Synchronized: box threads add to it (VulkanBuffer.realloc, staged uploads, renderer.cleanup += …) while the render thread swaps it out.
var cleanupStack
val isClosed: Boolean True after cleanup begins; between-frame producers must stop before using this device again. Volatile because the flag is read by afield.graphics.vulkan.buffers.VulkanBuffer.map on whichever thread writes a buffer, to refuse a write into memory this cleanup has freed.
fun whenCurrentGraphicsFrameComplete(callback: () -> Unit) Finish a GPU-side capture without waiting in the render callback. Call while recording a graphics frame; callback runs after that submission's fence signals. It must stay short (complete a future or queue IO), because frame retirement also runs other cleanup work.
data class ExternalSemaphorePoint(val semaphore: Long, val value: Long)
fun <T> runOnQueueThread(block: () -> T): T Run a block on the dedicated queue thread; if already on it, run inline.
suspend fun <T> onQueueThread(block: () -> T): T Suspend-friendly variant to hop to the queue thread.
var renderStart
var renderEnd
var currentCommandBuffer: CommandBuffer?
data class GeometryFrameToken(val frameId: Long, val imageIndex: Int, val windowIndex: Int)
inner class GeometryPhaseScope(val token: GeometryFrameToken, private val parallelAllowed: Boolean)
val geometryProducers
fun beginGeometryWrites(windowIndex: Int = 0): GeometryFrameToken Opens a geometry-write window for the current frame and returns the token used by pools.
fun endGeometryWrites(windowIndex: Int = 0) Explicitly closes a previously opened geometry-write window.
fun installRunloopGeometryBeginHook(runloop: Runloop = Runloop.runLoop, key: String = "renderer:${System.identityHashCode(this)}:beginGeometryWrites") Installs a Runloop pre-frame hook that opens geometry writes every runloop tick.
fun uninstallRunloopGeometryBeginHook(runloop: Runloop = Runloop.runLoop)
var openXRHmdTarget: afield.graphics.vulkan.targets.OpenXRHmdTarget? Optional OpenXR HMD target rendered once per frame (after window presents).
data class FrameMarkers(var tsFrameBegin: Int = -1, var tsAfterCompute: Int = -1, var tsAfterGraphics: Int = -1, var tsFrameEnd: Int = -1, var pipeStatsCompute: Int = -1, var pipeStatsGraphics: Int = -1, var perfFrameIndex: Int = -1)
data class Pending(val imageIndex: Int)
data class ModuleMarks(val name: String, var tsBegin: Int = -1, var tsEnd: Int = -1, var pipeStatsIndex: Int = -1, var perfIndex: Int = -1)
fun registerModuleMarks(imageIndex: Int, marks: ModuleMarks)
fun registerModuleMarks(windowIndex: Int, imageIndex: Int, marks: ModuleMarks)
fun cleanup()
var frameCounter Monotonic submitted-frame id; acquisition-only retries do not advance GPU retirement.
fun recordVertexBufferUpload(bytes: Long, immediate: Boolean)
fun supportsDirectWindowCapture(window: Window = this.window): Boolean
suspend fun captureWindowToFile(window: Window = this.window, outputPath: String, timeoutMs: Long = 15_000L): String
var presentationRevision Changes after a presentation gap; temporal adapters must reject old history.
fun render()
class ImageNotAvailable(s: String, val retryable: Boolean = false) : RuntimeException(s)
fun submit(context: WindowContext, commandBuffer: CommandBuffer, currentFence: Fence, queue: Queue, computeSemaphore: Semaphore?)
fun submitCompute(context: WindowContext, commandBuffer: CommandBuffer, currentComputeFence: Fence, signalSemaphore: Semaphore, queue: Queue)
var running
fun runForever(update: () -> Unit)
suspend fun run()
operator fun plusAssign(a: Pair<String, RayTracingModulePointer>)+8 overloads · also Pair<String, ModulePointer>, ModulePointer, Pair<String, MeshModulePointer>, MeshModulePointer, Named<ModulePointer>, Pair<String, ComputeModulePointer>, Named<ComputeModulePointer>, Named<RayTracingModulePointer>
fun getProfiler(): GpuProfiler?

SnapshotWindow.kt

src/jvmMain/kotlin/afield/graphics/vulkan/SnapshotWindow.kt — read the source

Top-level functions

Declaration Summary
fun Box.snapshotWindow(renderer: Renderer, frames: Int = 10, channel: String = "window", width: Int = 640, skipBlank: Boolean = false): Job Puts a picture of renderer's window into this box's channel output, once frames frames have been drawn after the call.

WindowCaptureQueue.kt

src/jvmMain/kotlin/afield/graphics/vulkan/WindowCaptureQueue.kt — read the source

DynamicBuffer.kt

The cursor a Vulkan buffer is written through: std430 padding, integers packed into float slots, and a buffer that grows under a write that would run past its end. DynamicBuffer is all of it. FLine geometry on its way to the GPU, the fabricated scene's shapes and a box's own VulkanBuffer.write { … } block all put their floats here.

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/DynamicBuffer.kt — read the source

DynamicBuffer

class DynamicBuffer(var target: FloatBuffer, var onOverrun: (FloatBuffer, Int) -> FloatBuffer)

A write cursor over GPU memory that pads as std430 does and grows when it runs out. This is what VulkanBuffer.write { … } and append { … } hand to their block, so a box streaming geometry or particle state writes put(…) calls and never computes an offset.

Declaration Summary
val at: Int How far the cursor has been written, counted in floats rather than bytes.
fun put(f: FloatBuffer, padding: Boolean = true): DynamicBuffer+1 overload · also IntBuffer Copies a buffer's remaining contents in bulk, after padding to a four-float boundary unless padding is false. A copy that would run past the end grows the target and is replayed, so it is never partial. The IntBuffer overload copies the integers' bits into the same float slots rather than converting them.
fun put(f: FloatArray, padding: Boolean = true, offset : Int = 0): DynamicBuffer Copies a float array from offset onwards, padding to a four-float boundary first unless padding is false.
fun put(d: DoubleArray): DynamicBuffer Writes each double narrowed to a float, one at a time, with no padding anywhere.
fun putInt(d: List<Int>): DynamicBuffer Writes each integer's bits into its own float slot, one after another, with no padding before or between them. The shader side reads them as a run of int or uint members.
fun lines(): (Boolean, Vec3) -> Unit Returns a callback that appends a line record as x, y, z, breakFlag.
fun putInt(n: Int): DynamicBuffer Writes one integer's thirty-two bits into the next float slot, with no padding. The shader reads the slot as an int or uint.
fun putScalar(n: Double): DynamicBuffer+1 overload · also Float Writes one value, narrowed to a float, wherever the cursor is. No alignment and no padding, which is what makes it the scalar case.
fun put(n: Float): DynamicBuffer+2 overloads · also Vec4, Vec2 Writes one float at the cursor, with no padding. The Vec2 and Vec4 overloads grouped here pad to their own alignment first.
fun put(n: Double, m: Double): DynamicBuffer+1 overload · also Float Writes a vec2: pads to a two-float boundary, then the two values narrowed to floats.
fun put(n: Double, m: Double, p: Double, zero: Float = 0f): DynamicBuffer+1 overload · also Float Writes three floats after padding to a four-float boundary, leaving the fourth slot of the vec4 for whatever is written next. The Float overload grouped with it is not the same: it does no padding, and it does write a fourth value, zero.
fun put(n: Vec3, zero: Float = 0f): DynamicBuffer Writes a padded vec3: aligns to a four-float boundary, then x, y, z and zero in the fourth slot, as std430 expects.
fun pad4(zero: Float = 0f) Advances the cursor to the next four-float boundary by writing up to three copies of zero, and writes nothing when it is already there. This is the alignment a vec4 or a vec3 needs, and the bulk put overloads call it for you unless passed padding = false.
fun pad2(zero: Float = 0f) Advances the cursor to the next two-float boundary, writing one zero when it is at an odd position. This is the alignment a vec2 needs, and the vec2 overloads of put call it.
fun putIntBitsIntoFloatBuffer(src: IntBuffer, dst: FloatBuffer) todo, we need to realloc if it doesn't fit

FrameBufferGroup.kt

FrameBufferGroup holds one Vulkan buffer per swapchain image, so data written for the current frame never overwrites a buffer an in-flight frame is still reading.

The BufferAccessPattern chosen at construction decides where the buffers live: host-visible memory written directly, device-local memory fed through staging buffers (once or every frame), or device-local memory with a host-visible copy for reading results back, host-cached memory read every frame, or device-local memory zero-filled when built. Writes go to the current image's buffer and are recorded with FrameBufferGroup.markAsWritten; FrameBufferGroup.getLastWritten returns the most recently written one.

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/FrameBufferGroup.kt — read the source

BufferAccessPattern

enum class BufferAccessPattern

Where a FrameBufferGroup's buffers live and how data moves between host and device.

Constants: HOST_VISIBLE, STAGED_WRITE_ONCE, STAGED_STREAM_WRITE, DEVICE_LOCAL_DEBUG_READ, HOST_VISIBLE_STREAM_READ, DEVICE_LOCAL_ZERO.

FrameBufferGroup

class FrameBufferGroup(val renderer: Renderer, val n: Int = renderer.numImages, val buffers: MutableList<VulkanBuffer> = mutableListOf(), val accessPattern: BufferAccessPattern = BufferAccessPattern.HOST_VISIBLE, private val stagedUploads: Boolean = accessPattern == BufferAccessPattern.STAGED_WRITE_ONCE || accessPattern == BufferAccessPattern.STAGED_STREAM_WRITE, private val readbackStaging: Boolean = accessPattern == BufferAccessPattern.DEVICE_LOCAL_DEBUG_READ) : HasBuffer, DynamicallyWriteable, BufferWriteable, BufferReadable

Declaration Summary
val renderer: Renderer The renderer the buffers belong to. It supplies the image currently being drawn, the image count, and the cleanup list the buffers are released on.
val n: Int How many buffers build makes. It defaults to one per swapchain image; pass 1 for data that does not change between frames.
val buffers: MutableList<VulkanBuffer> The buffers themselves, in image order. It is empty until build runs, and every lookup here takes its index modulo this list's size.
val accessPattern: BufferAccessPattern Where the buffers live and how data moves to and from them. It also sets the two staging flags below unless they are given explicitly.
var lastWritten The index into buffers of the one written most recently, or -1 before anything has been written. getLastWritten falls back to the current image's buffer while it is -1.
val usesStagedUploads: Boolean Whether host writes land in a separate staging buffer and reach the device through a copy. buildStaging must then have been called too.
val usesReadbackStaging: Boolean Whether reads come back through a host-cached copy rather than straight out of the buffer. buildReadback must then have been called too.
fun get(write : Boolean = false): VulkanBuffer The buffer for the image the renderer is drawing now. Passing write = true also records it as the most recently written.
fun getAt(imageIndex: Int, write: Boolean = false): VulkanBuffer The buffer for a given swapchain image. The index is taken modulo the number of buffers, so a group built with one buffer answers with it for every image.
fun read(builder: FloatBuffer.() -> Unit) mapAndRead under a shorter name, for a float view of the results.
fun read_ints(builder: IntBuffer.() -> Unit) mapAndRead_ints under a shorter name, for an integer view of the results.
fun getLastWritten() The buffer written most recently, which for a group written one frame and read the next is the one holding data. Before anything has been written it is the current image's buffer.
fun markAsWritten(imageIndex: Int = renderer.currentImage) Records an image index as the one most recently written, without writing anything. The write paths call it themselves; call it by hand after filling a buffer through some route this class does not know about.
fun build(builder: (Int) -> VulkanBuffer) Fills the group with n buffers, calling builder once per index. Any buffers already there are dropped from the list — without being released, so build once rather than to resize. A DEVICE_LOCAL_ZERO group also queues every new buffer for a zero fill, which the next clean records.
fun buildStaging(builder: (Int) -> VulkanBuffer) Builds the matching host-visible staging buffers, n of them. A group with usesStagedUploads needs these as well as the ones from build; nothing allocates them for you, and the DSL in magic.kt is what normally does both.
fun buildReadback(builder: (Int) -> VulkanBuffer) Builds the matching host-cached readback buffers, n of them, which a group with usesReadbackStaging copies into before a host read.
override fun getBuffer() The current image's VkBuffer handle, as it stands. No pending zero or upload is flushed first.
override fun getGPUAddress(cmd: CommandBuffer): Long The device address of the buffer written most recently, after any pending zero fill or staged upload has been recorded into cmd. This is the path a push constant takes, which is why binding a buffer is also what flushes it.
override fun write(builder: DynamicBuffer.() -> Unit): DynamicallyWriteable Writes the current image's buffer from the beginning, through a DynamicBuffer that pads as std430 does. On a staged group the block fills the staging buffer and the copy is queued for the next clean instead of happening here.
override val cursor : Int How far the current image's buffer has been written, in floats.
override fun append(builder: DynamicBuffer.() -> Unit): DynamicallyWriteable Continues writing the current image's buffer from where the last write or append to it stopped. The per-image cursor is the buffer's own, so appending across frames appends to whichever buffer this frame's image owns, not to a single running one.
override fun mapAndWrite(builder: FloatBuffer.() -> Unit): FrameBufferGroup Maps the current image's writable buffer and hands builder a float view of the whole allocation, no padding applied. On a staged group the upload is queued for the next clean.
override fun mapAndWrite_bytes(builder: ByteBuffer.() -> Unit): FrameBufferGroup mapAndWrite with a byte view of the same memory, for packed or mixed layouts.
override fun mapAndWrite_ints(builder: IntBuffer.() -> Unit): FrameBufferGroup mapAndWrite with an integer view of the same memory, for indices, counters and flags.
override fun clean(hostReading: Boolean, cmd: CommandBuffer?): FrameBufferGroup Carries out the zero fills and staged uploads that writes and build left pending.
override fun mapAndRead(builder: FloatBuffer.() -> Unit): FrameBufferGroup Reads the current image's buffer on the host as floats.
override fun mapAndRead_bytes(builder: ByteBuffer.() -> Unit): FrameBufferGroup mapAndRead with a byte view of the same memory.
override fun mapAndRead_ints(builder: IntBuffer.() -> Unit): FrameBufferGroup mapAndRead with an integer view of the same memory, which is how a compute counter comes back.

GpuSchema.kt

GpuSchema derives a std430 memory layout from an annotated Kotlin class, packs instances of it into buffers, and emits the matching GLSL struct and buffer_reference declarations.

Annotate a class with GpuStruct (and exclude members with GpuIgnore), then call gpuSchema to get its GpuSchema: field offsets, size and stride, GpuSchema.write and GpuSchema.writeArray for packing, GpuSchema.arrayView for indexed writes into a mapped buffer, and GpuSchema.glslDeclarations for the shader side. bindGpu binds a schema-backed value to a Structure member. Schemas are cached per class.

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/GpuSchema.kt — read the source

Used in: GPU schemas

Top-level functions

Declaration Summary
fun <T : Any> Structure.bindGpu(schema: GpuSchema<T>, value: () -> T) Bind a reflected Field Structure (including a push-constant block) from one schema value.
inline fun <reified T : Any> gpuSchema(): GpuSchema<T> The schema for T, built on first use and cached by class thereafter.
fun <T : Any> gpuSchema(type: KClass<T>): GpuSchema<T> The schema for a class known only at runtime, built on first use and cached the same way as the reified form.

GpuStruct

annotation class GpuStruct(val glslName: String = "")

Marks a Kotlin class as a CPU/GPU ABI declaration.

GpuIgnore

annotation class GpuIgnore

A constructor property that exists on the Kotlin value but is not part of its GPU representation.

GpuPrimitiveKind

enum class GpuPrimitiveKind(val glslType: String, val alignmentBytes: Int, val sizeBytes: Int)

The scalar/vector leaves understood by the first GPU-schema ABI.

Constants: FLOAT, INT, UINT, INT64, UINT64, VEC2, VEC3, VEC4.

Declaration Summary
val glslType: String How this leaf is spelled in the generated GLSL.
val alignmentBytes: Int The std430 base alignment: a field of this kind starts at the next multiple of it.
val sizeBytes: Int How many bytes the value occupies, which for vec3 is 12 against an alignment of 16, so a scalar may follow it at +12.

GpuFieldType

sealed class GpuFieldType

Declaration Summary
abstract val alignmentBytes: Int The std430 base alignment of this field, whether leaf or nested struct.
abstract val sizeBytes: Int How many bytes this field occupies. For a nested struct it is that struct's rounded size.
abstract val glslType: String The GLSL type name written for this field: a built-in for a leaf, the struct's own name for a nested one.
data class Primitive(val kind: GpuPrimitiveKind) : GpuFieldType() A scalar or vector leaf, taking its layout straight from the kind.
data class Struct(val schema: GpuSchema<*>) : GpuFieldType() Another @GpuStruct laid out inline at this field's offset, by its own schema. It is the bytes, not a pointer to them.

GpuField

data class GpuField(val name: String, val offsetBytes: Int, val type: GpuFieldType, internal val property: KProperty1<Any, *>)

Declaration Summary
val name: String The Kotlin property's name, which is used unchanged as the GLSL field name.
val offsetBytes: Int Where the field starts, in bytes from the beginning of the struct.
val type: GpuFieldType Whether this is a leaf or a nested struct, and the layout that follows from it.
val sizeBytes: Int How many bytes the field occupies, from its type.
val alignmentBytes: Int The field's std430 base alignment, from its type.

GpuSchema

class GpuSchema<T : Any> internal constructor(val kotlinClass: KClass<T>, val glslName: String, val fields: List<GpuField>, val alignmentBytes: Int, val sizeBytes: Int)

One reflected, deterministic std430-compatible memory schema.

Declaration Summary
val kotlinClass: KClass<T> The annotated Kotlin class this layout was reflected from, and the key it is cached under.
val glslName: String The name of the generated GLSL struct: the one given to the annotation, or the class's simple name.
val fields: List<GpuField> The fields in primary-constructor order, with @GpuIgnore ones left out. That order is the ABI, so reordering the constructor changes the layout.
val alignmentBytes: Int The struct's own base alignment, the largest of its fields', or 1 for a struct with no fields.
val sizeBytes: Int The struct's size in bytes, the end of the last field rounded up to alignmentBytes.
val strideBytes: Int Struct arrays use the struct's rounded size as their std430 stride.
val referenceAlignmentBytes: Int buffer_reference_align is a promise made by the shader; 16 is a conservative BDA baseline.
val abiSignature: String by lazy The layout written out in full: the struct's name, alignment and size, then every field's name, offset and type. Two schemas agree on memory exactly when these strings match, and comparing them says which field moved.
val abiHash: String by lazy The first eight bytes of the signature's SHA-256, as sixteen hex characters: short enough to carry in a shader template or a log line as a layout version.
fun field(name: String): GpuField The named field. A name this struct does not have is an error that names the struct, not a null.
fun offsetOf(name: String): Int The named field's byte offset from the start of the struct.
fun offsetOf(property: KProperty1<T, *>): Int The field's byte offset, named by property reference, so a renamed Kotlin property carries its GLSL field name with it.
fun addressOf(baseAddress: Long, index: Int = 0): Long The device address of element index of an array of this struct starting at baseAddress, at strideBytes spacing. baseAddress is taken as the start of the array itself; nothing here knows about an offset into a larger buffer.
fun addressOf(baseAddress: Long, index: Int, field: String): Long The device address of one field of element index, for a shader that reads a single member rather than the whole struct.
fun write(value: T, target: ByteBuffer, baseOffset: Int = 0) Pack one value without changing target's position or byte order.
fun writeArray(values: Iterable<T>, target: ByteBuffer, baseOffset: Int = 0): Int Pack values consecutively at strideBytes spacing. Returns the number written.
fun arrayView(target: ByteBuffer, count: Int, baseOffset: Int = 0): GpuArrayView<T> A bounds-checked view for writing count values into target at this struct's stride. The capacity is checked once, here, rather than on each write.
fun writer(value: () -> T): WritesToBuffer Adapter for Structure.bindAll { ... }, including whole push-constant blocks.
fun glslStructDeclarations(): String All regular struct declarations needed by this schema, nested dependencies first.
fun glslReferenceDeclaration(): String The buffer_reference block for a pointer to a single value of this struct, named after it with Ref appended.
fun glslArrayReferenceDeclaration(): String The buffer_reference block for a pointer to an unsized array of this struct, named after it with Array appended.
fun glslDeclarations(): String GLSL intended for {{ Particle.all }} directly after #version.
val template: Map<String, Any> A value suitable for shaderTemplate["Particle"] = schema.template.

GpuArrayView

class GpuArrayView<T : Any> internal constructor(val schema: GpuSchema<T>, private val target: ByteBuffer, val count: Int, val baseOffset: Int)

A CPU view over an array whose stride is governed by one GpuSchema.

Declaration Summary
val schema: GpuSchema<T> The layout every element is written with, and the stride between them.
val count: Int How many elements the view covers. Every index is checked against it.
val baseOffset: Int Where element zero starts, in bytes from the beginning of the target buffer.
operator fun set(index: Int, value: T) Packs value over element index, leaving the target buffer's own position and byte order alone.
fun byteOffsetOf(index: Int): Int Where element index starts, in bytes from the beginning of the target buffer, baseOffset included.
fun addressOf(baseAddress: Long, index: Int): Long The device address of element index, given the address the target buffer begins at. This counts baseOffset in, unlike the schema's own addressOf, which measures from the array.

ParallelGeometryPool.kt

ParallelGeometryPool lets several writers produce geometry for the same frame, each into its own per-swapchain-image buffers, and dispatches a module once per writer that published.

See ParallelGeometryPool for the frame-phase rules, writer publication and how buffer_reference push constants switch a module into fan-out mode.

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/ParallelGeometryPool.kt — read the source

ParallelGeometryPool

class ParallelGeometryPool(val renderer: Renderer = fromContext(), val targetWindowIndex: Int = 0, val idleRetainFrames: Long = 120, private val initialSizeBytes: Long = 4L * 1024L, private val usageFlags: Int = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT or VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT, private val memoryFlags: Int = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)

Per-writer, per-swapchain-image geometry buffers that fan one module invocation out into one invocation per writer published for the current renderer frame.

Declaration Summary
val renderer: Renderer The renderer whose geometry phase the pool follows. Constructing the pool registers it there, and cleanup is what takes it off again.
val targetWindowIndex: Int Which window's swapchain images the channel buffers are sized for. Geometry tokens for any other window are ignored, so a pool only ever sees its own window's frames.
val idleRetainFrames: Long How many frames a writer slot may go untouched before it is dropped at the next seal and its buffers queued on the renderer's cleanup list. A writer that is still publishing is never dropped; one that falls silent costs this many frames of buffers before it is reclaimed.
data class WriterPublication(val vertexCount: Int? = null, val indexCount: Int? = null, val dispatchX: Int? = null, val dispatchY: Int? = null, val dispatchZ: Int? = null) What one writer asked its fan-out invocation to draw or dispatch this frame. A null count is not zero: it leaves the module's own draw or dispatch dimension in place for that writer.
class ActiveWriter internal constructor(val writerId: Long, val publication: WriterPublication, internal val channels: Map<String, FrameBufferGroup>) One writer as the frame sealed it: its id, what it published, and the channel buffers it had at that moment. The channel map is copied at seal, so a later write cannot change what this frame's recording sees.
data class SealedSnapshot(val token: Renderer.GeometryFrameToken, val writers: List<ActiveWriter>) The whole sealed writer list for one frame, in ascending writer-id order, together with the frame and swapchain image it was sealed against. Recording for a different frame or image finds no snapshot rather than a stale one.
fun writer(writerId: Long = Thread.currentThread().threadId()): ParallelGeometryWriter A handle for writing and publishing under one writer id, by default the calling thread's.
fun ref(channel: String): ParallelGeometryRef A reference to channel that resolves per writer at command-recording time, for binding to a buffer_reference push constant. Two references to the same channel of the same pool compare equal, so one can be rebuilt rather than held.
fun cleanup() Takes the pool off the renderer, drops every writer slot, and queues all of their channel buffers on the renderer's cleanup list. The pool takes no further part in any frame; a write after this one fails for want of an active phase rather than silently going nowhere.

ParallelGeometryWriter

class ParallelGeometryWriter internal constructor(private val pool: ParallelGeometryPool, val writerId: Long)

A stable writer-id view into one ParallelGeometryPool.

Declaration Summary
val writerId: Long The id this handle writes under. It identifies the pool slot, and it orders this writer among the frame's fan-out invocations.
fun write(channel: String, block: DynamicBuffer.() -> Unit) Replace this frame image's channel contents.
fun append(channel: String, block: DynamicBuffer.() -> Unit) Append to this frame image's existing channel contents.
fun publish(vertexCount: Int? = null, indexCount: Int? = null, dispatchX: Int? = null, dispatchY: Int? = null, dispatchZ: Int? = null) Make this writer live for the current frame, optionally overriding the module's normal draw or dispatch dimensions. Publishing without writing is allowed and reuses retained channels.

ParallelGeometryRef

class ParallelGeometryRef internal constructor(val pool: ParallelGeometryPool, val channel: String) : HasBuffer

Late-bound reference to one channel of the writer currently being recorded by fan-out dispatch. Resolving it outside that dispatch, or for a writer that lacks channel, is an error.

Declaration Summary
val pool: ParallelGeometryPool The pool this reference resolves against. Every parallel reference in one push-constant block must name this same pool.
val channel: String The channel name looked up in the writer being recorded. A writer that never wrote this channel fails its own invocation.
override fun getBuffer(): Long The current writer's buffer for channel, for this frame's swapchain image.
override fun getGPUAddress(cmd: CommandBuffer): Long The device address of the current writer's channel buffer, which is what the push constant carries.
override fun equals(other: Any?): Boolean Same pool object and same channel name. The pool is compared by identity, the channel by value.
override fun hashCode(): Int Built from the pool's identity hash and the channel name, to match equals.

Structure.kt

Structure maps a shader's introspected uniform block or push-constant block onto named Kotlin members, so a box can set, read and bind values by member name.

A Structure is built from the Introspected.Member list that shader reflection produces, and writes each value at its reflected offset when the block is uploaded. Values can be set directly, or bound to a function that is re-evaluated each time the block is written (bindPushConstant, HasUniform); a ProgressingUniform keeps being written until it reports completion. A member that nothing has been assigned to fails with unassignedMemberMessage.

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/Structure.kt — read the source

Used in: GPU schemas, Running code in a box

Top-level functions

Declaration Summary
fun unassignedMemberMessage(member: Any?): String The message for a shader member that has nothing bound when a dispatch or draw needs it. It is not a missing buffer: nothing has been assigned to the member yet, and the usual reason is a binding written inside loop {}, which takes effect a frame after the first dispatch.
fun <T : Structure> HasBuffer.asStructure(structure: T, members: List<Introspected.Member>): T
fun <T : Structure> BufferParameters.asStructure(structure: T, members: List<Introspected.Member>): T
fun <T> Structure.setPushConstant(name: String, value: T, notFound: () -> Nothing) Sets a push-constant value by name, or invokes notFound if name isn't a known offset. Shared by the FromStructure/BoundFromStructure delegates on Structure's various hosts (Mesh, MeshMesh, ComputeMesh, RayTracingDispatch, and their groups), which otherwise each hand-rolled this same offset-check-then-write shape.
fun <T> Structure.getPushConstant(name: String, notFound: () -> Nothing): T Reads a push-constant value by name, or invokes notFound if name isn't a known offset.
fun Structure.bindPushConstant(name: String, value: Function0<*>, notFound: () -> Nothing) Binds a live push-constant supplier by name, or invokes notFound if name isn't a known offset.
fun <T> Structure.getBoundPushConstant(name: String, notFound: () -> Nothing): () -> T Reads a bound push-constant supplier by name, or invokes notFound if name isn't a known offset.
fun <E> Iterable<E>.bindPushConstantAcrossGroup(name: String, value: Function0<*>, push: (E) -> Structure, ensureInitialized: (E) -> Unit, notFound: (E) -> Nothing) Binds value as a push constant on every element of a group, initializing each element's offsets on first use via ensureInitialized. Throws (via notFound, given the offending element) if some element in the group doesn't have name.
fun <E, T> Iterable<E>.firstBoundPushConstant(name: String, push: (E) -> Structure, ensureInitialized: (E) -> Unit, notFound: () -> Nothing): () -> T Finds the first element of a group that has name bound, initializing each element's offsets on first use via ensureInitialized. Throws notFound if no element has it.

HasUniform

interface HasUniform<T> : () -> T

A uniform value computed on demand: evaluateUniform is called each time the owning Structure is written.

Declaration Summary
fun evaluateUniform(): T
override fun invoke()

ProgressingUniform

interface ProgressingUniform

Marker for uniforms that progress over time and need to keep being written until they report completion.

Declaration Summary
val isComplete: Boolean

WritesToBuffer

interface WritesToBuffer

Declaration Summary
fun writeTo(b : ByteBuffer, baseOffset : Int)

MissingRefException

class MissingRefException(override val message: String) : Exception(message)

Structure

open class Structure(var members: List<Introspected.Member> = emptyList(), private val baseOffset: Int = 0, private val pathPrefix: String = "", private val root: Structure? = null) : BufferWriteable, BufferReadable, HasBuffer

Declaration Summary
var members: List<Introspected.Member>
class FromStructure<T>
var nullPointerChecks: MutableMap<String, Boolean>
var offsets
var sizes
var pointers
val bindings: MutableMap<String, Function0<*>>
val values: MutableMap<String, Any>
var allBinding : (() -> WritesToBuffer)?
var dirty: Boolean Whether some buffer slot may still hold older values than this structure — setting it true is markDirty; setting it false records every slot as current.
fun needsWrite(slot: Int): Boolean Whether buffer slot needs this structure written into it: it has live bindings (evaluated per write), a whole-block binding, or a change newer than the last write to that slot.
fun writeIfStale(target: FrameBufferGroup, cmd: CommandBuffer?, autowireReferences: (String) -> () -> HasBuffer) Write this structure into target's buffer for the current swapchain image, if that buffer is stale (needsWrite). The one path a module's per-frame uniform blocks are written by.
var proxying: FrameBufferGroup? Legacy group binding, sharing the backing buffer used by mapping and nested structures.
fun setMembers(members: List<Introspected.Member>): Structure
operator fun set(property: String, value: Any) Set property to value, held until the next write to the buffer.
var reportTo: ((String) -> Unit)? Where a write this structure refuses is reported (see refusalFor). A module points its uniform structures at its error handler, which puts the message on the box; unset, it goes to the log.
fun bind(property: String, value: Function0<*>)
fun bindAll(value : () -> WritesToBuffer)
operator fun <T> get(property: String)
fun bindingFor(property: String): Function0<*>? The live supplier for property if it is bound, or null if it is held as a plain value or is not held at all — get without invoking the binding, for a caller that wants the supplier itself. Honours this structure's nested path, which reading bindings directly does not.
fun valueFor(property: String): Any? The plain value for property if it is held as one, or null if it is bound or absent — the other half of bindingFor. A bound property reads null here even though get returns something.
fun setProxyFor(p: FrameBufferGroup)
fun proxyTo(p: HasBuffer): Structure Forwards this structure's buffer access to p and returns this structure for chaining.
fun <T : Structure> childStructure(property: String, builder: (List<Introspected.Member>, Int, String, Structure) -> T): T
override fun mapAndWrite(builder: FloatBuffer.() -> Unit): Structure
fun write(name: String, builder: FloatBuffer.() -> Unit)
override fun mapAndWrite_bytes(builder: ByteBuffer.() -> Unit): Structure
override fun mapAndWrite_ints(builder: IntBuffer.() -> Unit): Structure
override fun clean(hostReading: Boolean, cmd: CommandBuffer?): Structure
fun primeBufferReferences(cmd: CommandBuffer, autowireReferences: (String) -> () -> HasBuffer)
fun writeToBuffer(f: ByteBuffer, cmd : CommandBuffer?, autowireReferences: (String) -> () -> HasBuffer)
override fun mapAndRead(builder: FloatBuffer.() -> Unit): Structure
override fun mapAndRead_bytes(builder: ByteBuffer.() -> Unit): Structure
override fun mapAndRead_ints(builder: IntBuffer.() -> Unit): Structure
override fun getBuffer(): Long
override fun getGPUAddress(cmd: CommandBuffer): Long

StructureTypeCheck.kt

Decides what a Kotlin value set on a shader's uniform member is written as, or why the shader cannot be given it.

A Structure is set by member name, and the member that name refers to is whatever the shader's Introspection last reported. Value and slot can therefore disagree: a Vector3f on a member the shader now declares float, an Int on a float, a Float on a float16_t. Judged by the value's Kotlin type alone, each of those writes the wrong number of bytes, or the right number meaning something else.

The rule is to convert what converts without loss and to refuse a different shape. Shape is checked first: a scalar belongs in a scalar, a vector in a vector of the same length, a matrix in a matrix of the same size, and nothing but a buffer-writing value in a struct. Within one shape the value is converted to the type the member declares whenever that type can hold it, which covers any integer into a wider one, a narrower or differently signed integer that is in range, an integer into float while it stays exactly representable, a whole float into an integer, and a Double into a float. A float set on a half is rounded to the half's precision, which is what the member asked for. Anything else is refused, naming the value that would not fit. Array members, device pointers and values whose width is not knowable here are written as given.

Three callers ask this question. Structure.set asks it when the value arrives and declines to store a value it would not be able to write, so the answer reaches whoever is typing. Writing a block to its buffer asks it per member, and skips the member rather than writing nonsense into the slot. Structure.carryValuesFrom asks it of every value a shader reload is trying to keep, which is how a reload reports the uniforms it had to drop. Each distinct refusal is reported once, not once a frame.

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/StructureTypeCheck.kt — read the source

VBuffer.kt

VertexBuffer is a classic vertex buffer with a host-side staging copy that is transferred to device-local memory before drawing.

Most Field geometry instead pulls vertices in the shader through device addresses set in push constants, which needs no staging step; VertexBuffer remains for code that binds vertex buffers directly.

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/VBuffer.kt — read the source

VertexBuffer

class VertexBuffer(val renderer: Renderer, val size: Long, val usage: Int = VK10.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, val createCPU: Boolean = false) : HasBuffer, BufferWriteable

this is, as far as I can tell, completely decoy — with resizable BAR (or unified memory on Apple Silicon) this staging buffer thing is just excess engineering and writing shaders that do their own pulling is better and device pointers set through push constants is faster and architecturally skips all of the descriptor nonsense

Declaration Summary
val renderer: Renderer The renderer whose device both allocations are made on, and whose graphics queue carries the copy.
val size: Long Bytes. Both the staging and the device-local allocation are this size, for the life of the buffer; trimTo narrows what is copied, not what is allocated.
val usage: Int The VkBufferUsageFlags the device-local buffer carries beyond transfer-destination. It also decides which reads the transfer barriers cover: vertex-attribute reads for a vertex buffer, index reads for an index buffer, and no barrier at all for a usage with neither bit.
val createCPU: Boolean Whether to allocate the spare host-side cpu buffer. Nothing in the transfer path reads it.
val cpu A plain host size-byte buffer, allocated only when createCPU was asked for, and null otherwise. It is not the staging buffer and is not part of the upload: nothing here writes to it or reads from it, and the renderer's buffer inspector only reports whether it exists.
val src The host-visible, host-coherent staging buffer. The mapAndWrite family writes here, and a transfer copies it into dest.
val dest The device-local buffer the GPU draws from, carrying usage on top of transfer-destination. This is the handle getBuffer returns.
var watermark Counts host writes: every mapAndWrite call raises it by one, whether or not the block it ran completed.
var transferWatermark The watermark the last transfer copied. While the two differ the device-local buffer is behind the staging one, and clean or transfer will copy; a transfer sets them equal. It starts at -1 so that a buffer written once before its first transfer is copied.
var neededSize How many bytes a transfer copies, starting at the whole size and narrowed by trimTo. At zero or less the copy and both barriers are skipped entirely, so a trimmed-to-nothing buffer still reports itself clean after a transfer.
override fun toString(): String Size, usage and whether the staging copy is still ahead of the device one, as DIRTY or clean.
override fun clean(hostReading: Boolean, cmd: CommandBuffer?): VertexBuffer Brings the device-local buffer up to date with the staging one, if it is not already.
override fun mapAndWrite(builder: FloatBuffer.() -> Unit): VertexBuffer Maps the staging buffer and hands builder a float view of the whole allocation, then unmaps and marks the device copy out of date. The view holds size / 4 floats and starts at position zero on every call, so a partial write leaves the rest of the previous contents in place. The buffer is unmapped and the watermark raised even if the block throws.
override fun mapAndWrite_ints(builder: IntBuffer.() -> Unit): VertexBuffer mapAndWrite with an integer view of the same memory, size / 4 ints wide. Index data goes here.
override fun mapAndWrite_bytes(builder: ByteBuffer.() -> Unit): VertexBuffer mapAndWrite with a byte view of the same memory, the full size. Use it for packed or mixed vertex formats.
fun transfer(cmd: CommandBuffer, immediate: Boolean = false) Records the staging-to-device copy into cmd, bracketed by the two barriers that move the buffer between vertex input and transfer, and then counts it as clean. A buffer already at its transfer watermark records nothing.
fun transferNow() Does the transfer on a command buffer of its own and blocks until the GPU has finished it.
fun cleanup() Queues both allocations for release on the renderer's cleanup list rather than freeing them here, so a frame still reading this buffer finishes first. The buffer is unusable from the moment that list runs, not from the moment this is called.
fun trimTo(neededSize: Long) Narrows the copy to the first neededSize bytes, for the common case of a buffer allocated for a worst-case vertex count and filled with fewer. It changes neither allocation, and it takes effect on the next transfer rather than marking the buffer dirty.
override fun getBuffer(): Long The device-local dest handle, which is what a draw binds; the staging buffer is never handed out.
override fun getGPUAddress(cmd : CommandBuffer): Long Always throws: neither allocation is created with the device-address usage bit, so this buffer cannot be reached by pointer. Shader-pulled geometry wants a VulkanBuffer built with addressable() instead.

VulkanBuffer.kt

A block of GPU memory, and the ways to hand one to a shader. VulkanBuffer is the allocation itself: VMA-suballocated, kept mapped on the CPU side for its whole life, resizable while frames are in flight, and released through the renderer's frame-retirement queue rather than on the spot. Vertex data, uniforms, storage buffers, acceleration-structure scratch and compute input and output are all this one class under different usage flags.

Writing and reading go through three small interfaces, and which you reach for is a question of how much you want to think about layout. BufferWriteable and BufferReadable map the allocation as a plain FloatBuffer, ByteBuffer or IntBuffer and let you address it yourself. DynamicallyWriteable gives you a DynamicBuffer instead, which pads to std430 as it goes and grows the allocation under a write that would overflow it; write { … } starts at the beginning and append { … } continues from the cursor.

HasBuffer is the handle a shader is bound to, and the useful thing about it is that it composes. Adding a Long to one offsets it. zeroBefore on a buffer, and barrier or graphicsBarrier on any handle, return a handle that records that work on the way to giving the address up. "This buffer, cleared first" and "this buffer, once the compute writes to it are visible" then travel with the buffer, so nothing has to remember where in the frame to put the clear or the barrier.

Render workers read the handle, address and descriptor range while recording a frame, on other threads than the one that writes them. Every one of those fields is therefore @Volatile, and allocation builds its replacements in locals and publishes them at the end, handle before range. A worker sees either the old allocation, which is still alive, or the new one, and never a null handle mid-resize.

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/VulkanBuffer.kt — read the source

Top-level functions

Declaration Summary
operator fun HasBuffer.plus(offset: Long): HasBuffer

Top-level values

Declaration Summary
val VulkanBuffer.zeroBefore: HasBuffer
val HasBuffer.barrier: HasBuffer
val HasBuffer.graphicsBarrier: HasBuffer

HasBuffer

interface HasBuffer

A GPU buffer handle and the device address a shader reads it at.

Declaration Summary
fun getBuffer(): Long
fun getGPUAddress(cmd: CommandBuffer): Long

BufferWriteable

interface BufferWriteable

Declaration Summary
fun mapAndWrite(builder: FloatBuffer.() -> Unit): BufferWriteable
fun mapAndWrite_bytes(builder: ByteBuffer.() -> Unit): BufferWriteable
fun mapAndWrite_ints(builder: IntBuffer.() -> Unit): BufferWriteable
fun clean(hostReading: Boolean, cmd: CommandBuffer? = null): BufferWriteable

DynamicallyWriteable

interface DynamicallyWriteable

Declaration Summary
fun write(builder: DynamicBuffer.() -> Unit): DynamicallyWriteable
fun append(builder: DynamicBuffer.() -> Unit): DynamicallyWriteable
val cursor : Int

BufferReadable

interface BufferReadable

Declaration Summary
fun mapAndRead(builder: FloatBuffer.() -> Unit): BufferReadable
fun mapAndRead_bytes(builder: ByteBuffer.() -> Unit): BufferReadable
fun mapAndRead_ints(builder: IntBuffer.() -> Unit): BufferReadable
fun clean(hostReading: Boolean, cmd: CommandBuffer?): BufferReadable

VulkanBuffer

class VulkanBuffer(val renderer: Renderer = fromContext(), var requestedSize: Long, val usage: Int, val reqMask: Int, val needsDeviceAddress: Boolean = false, val cpuOnly: Boolean = false) : HasBuffer, BufferWriteable, BufferReadable, DynamicallyWriteable

Declaration Summary
val renderer: Renderer
var requestedSize: Long
val usage: Int
val reqMask: Int
val needsDeviceAddress: Boolean
val cpuOnly: Boolean
var gpuAddress: Long
var allocationMemoryType: Int
var allocationMemoryFlags: Int
var buffer: Long
class DescriptorBinding internal constructor(val buffer: Long, val range: Long) The handle and range a descriptor write needs, published as one object so a render worker never pairs one allocation's handle with another's size. Replaced (never mutated) by allocate, realloc, cleanup and pushCleanup.
var hint_read: Boolean
var hint_write: Boolean
var watermark: Long
var readWatermark: Long
var writeWatermark: Long
var zeroBeforeComputation
var source: BufferParameters?
fun realloc(bytes: Long) Grow-resize with deferred destruction of the old buffer/allocation.
fun dynamicResizer() Optional helper you already used for dynamic growth.
fun zero(cmd: CommandBuffer) Fill with zeros then make the writes visible to subsequent compute shader reads/writes using synchronization2. If you need this to precede graphics-stage use instead, change dstStageMask to the appropriate stage(s).
override fun clean(hostReading: Boolean, cmd: CommandBuffer?): VulkanBuffer
fun descriptorBinding(): DescriptorBinding The handle and range to write into a descriptor for this buffer, read once, as a pair.
fun map(): Long Map via VMA (or host-only); keep persistently mapped (recommended).
fun unmap(offset: Long = 0, size: Long = requestedSize) Flush writes for non-coherent memory. We keep it mapped.
override fun mapAndWrite(builder: FloatBuffer.() -> Unit): VulkanBuffer
var writePosition
override val cursor: Int
override fun write(builder: DynamicBuffer.() -> Unit): VulkanBuffer
override fun append(builder: DynamicBuffer.() -> Unit): VulkanBuffer
override fun mapAndWrite_bytes(builder: ByteBuffer.() -> Unit): VulkanBuffer
override fun mapAndWrite_ints(builder: IntBuffer.() -> Unit): VulkanBuffer
override fun mapAndRead(builder: FloatBuffer.() -> Unit): VulkanBuffer
override fun mapAndRead_bytes(builder: ByteBuffer.() -> Unit): VulkanBuffer
fun mapAndReadBytes(size: Long, builder: ByteBuffer.() -> Unit): VulkanBuffer
override fun mapAndRead_ints(builder: IntBuffer.() -> Unit): VulkanBuffer
fun pushCleanup() Same as cleanup, which now always retires through the renderer's queue while the renderer is alive. Kept for the callers that used it to ask for exactly that.
fun cleanup() Release this buffer.
val isReleased: Boolean True once cleanup has released this buffer (its handles may not be destroyed yet).
class Cleaner(val renderer: Renderer, val cbuffer: Long, val callocation: Long, val chostBuffer: ByteBuffer?) : Runnable
override fun toString(): String
val descriptorRange: Long
override fun getBuffer(): Long
override fun getGPUAddress(cmd: CommandBuffer): Long

magic.kt

The buffer DSL: buffer and defaultBuffer build a BufferParameters that says how a Vulkan buffer is sized, placed in memory and used.

defaultBuffer starts from host-visible, storage and device-addressable memory, which suits hardware with resizable BAR or unified memory; buffer starts from nothing. Size helpers such as 10.floats or 4.vec4s count std430 bytes (a vec3 takes 16). linkBuffer looks for an existing BufferParameters of the same name in the box context and merges new settings into it, so boxes can share one buffer.

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/magic.kt — read the source

Top-level functions

Declaration Summary
fun buffer(from: Introspected.UBO? = null, name: String = from?.name ?: "unnamed", parameters: BufferParameters.() -> Unit): BufferParameters Builds a BufferParameters named name, optionally sized from shader reflection from, and applies parameters to it.
fun defaultBuffer(from: Introspected.UBO? = null, name: String = from?.name ?: "unnamed", parameters: BufferParameters.() -> Unit): BufferParameters buffer, but starting from host-visible, storage and device-addressable before parameters runs. That is the combination to reach for on hardware with resizable BAR or unified memory, where the host can write straight into memory the GPU reads by pointer.
fun linkBuffer(name: String, initialSettings: BufferParameters): Pair<Boolean, BufferParameters> Looks for a BufferParameters called name in the box context, so boxes beneath a common parent can write to and read from one buffer.

BufferParameters

class BufferParameters(var source: Introspected.UBO? = null, var name: String = source?.name ?: "unnamed")

Declaration Summary
var source: Introspected.UBO? The reflected shader block this buffer is for, when there is one. It supplies the default name and is recorded on every buffer allocated from these parameters, which is what later bindings compare themselves against; sizing from it is the caller's own size = source.size.
var name: String What the buffer is called: the name linkBuffer matches in the box context so two boxes can reach the same allocation, and the name the incompatibility message quotes.
var perFrame: Boolean Whether to allocate one buffer per swapchain image rather than a single shared one, so the CPU can write next frame's contents while the GPU still reads this frame's. streamWrites sets it; writeOnce clears it.
val Int.floats: Long n.floats — the std430 size in bytes of n floats.
val Int.ints: Long n.ints — the std430 size in bytes of n 32-bit integers.
val Int.bytes: Long n.bytes — n, for a size already counted in bytes.
val Int.vec2s: Long n.vec2s — the std430 size in bytes of n vec2s, 8 apiece.
val Int.vec3s: Long n.vec3s — 16 bytes apiece, not 12: std430 pads a vec3 out to a vec4, and a host-side writer that packs them tightly will be read back misaligned.
val Int.vec4s: Long n.vec4s — the std430 size in bytes of n vec4s, 16 apiece.
val customize The blocks that configure these parameters, run in order at allocation rather than when they are added, so a size written in terms of a count the box has not computed yet still works. mergeFrom puts a donor's blocks in front of these.
var inlined Whether the contents live in plain host memory, pushed inline where they are used, instead of in a VkBuffer the GPU reads for itself. Such a buffer has nothing to bind to a descriptor and no device address, so asking for addressable as well throws.
var size: Long Size of the buffer in bytes; the n.floats-style helpers above count std430 bytes for a given element count.
var usage: Int The raw VkBufferUsageFlags. Set it through storage, uniform, indirect, addressable and the transfer pair dest() and source(), each of which adds its bit rather than replacing the field.
var memory: Int The raw VkMemoryPropertyFlags the allocation is placed by. hosted, writeOnce, streamWrites, streamReads and zeroInitialized each set a coherent combination of this and accessPattern.
var needsDeviceAddress Whether the buffer's GPU address is fetched after allocation, so a shader can reach it by pointer instead of through a descriptor. addressable sets it.
var accessPattern: BufferAccessPattern How the buffer is written and read over its life. It is compared exactly when an existing allocation is offered for this binding, so a buffer tuned for readback will not stand in for a plain hosted one even though both are host visible.
var stagedUploads Whether writes go through a host-visible staging buffer and a transfer, rather than into device memory the host can see directly. Allocates a second buffer of the same size.
var readbackStaging Whether a host-cached buffer is allocated to read results back into, which is what readOccasionallyForDebugging is asking for.
var postAdaptation Blocks run on each VulkanBuffer as it is created, while the group is still being built, so perFrame runs them once per swapchain image. Nothing in the tree registers one yet.
var result: FrameBufferGroup? The allocation these parameters produced, held so that asking for the buffer twice does not allocate twice. Set it yourself to bind an allocation you already have, and toBuffer will refuse it rather than silently allocate a fresh one if its parameters do not match.
fun storage() Adds the storage-buffer usage bit, so a shader can bind it as an SSBO.
fun inlined() Marks the contents as pushed inline where they are used rather than living in a VkBuffer. It cannot be combined with addressable, which toBuffer refuses.
fun addressable() Adds the device-address usage bit, and the storage bit with it, and asks for the address to be fetched once the buffer is allocated. This is what lets a shader reach the buffer through a buffer_reference pointer instead of a descriptor; it cannot be combined with inlined().
fun uniform() Adds the uniform-buffer usage bit, for a small block bound as a UBO.
fun hosted() Puts the buffer in memory the host can write directly, with no staging copy in either direction. It adds the host-visible memory bit without clearing any other, so a buffer moved here from a device-local pattern keeps that bit as well.
fun notHosted() Clears the host-visible and host-coherent memory bits. It touches neither the access pattern nor the staging flags, so it narrows where the allocation may be placed rather than switching the buffer to a staged scheme; writeOnce and the rest are what do that.
fun writeOnce() Device-local memory, filled once from a host staging buffer. It adds the transfer-destination usage bit, clears the host memory bits, and asks for a single buffer rather than one per swapchain image, since the contents do not change between frames.
fun stagedOnce() writeOnce under its other name.
fun streamWrites() Device-local memory, refilled from a host staging buffer every frame. Same as writeOnce except that it asks for one buffer per swapchain image, so the host can write next frame's contents while the GPU still reads this frame's.
fun stagedStream() streamWrites under its other name.
fun readOccasionallyForDebugging() Device-local memory with a host-cached copy to read results back into. It adds the transfer-source usage bit and clears the host memory bits; a buffer that is already written through staging keeps that access pattern and gains only the readback.
fun debugReads() readOccasionallyForDebugging under a shorter name.
fun occasionalReads() readOccasionallyForDebugging under a shorter name.
fun streamReads() Host-visible, host-cached memory read every frame, with no staging in either direction. The cached bit is what makes repeated host reads bearable; without it a host read of write-combined memory is very slow.
fun zeroInitialized() Device-local memory whose contents start at zero. It adds the transfer-destination usage bit, because the zeroing is a fill recorded by the buffer group the first time the buffer is cleaned rather than anything the host writes.
fun zeroInit() zeroInitialized under a shorter name.
fun indirect() Adds the indirect-buffer usage bit, so a draw or dispatch can read its own counts out of this buffer.
fun dest() Adds the transfer-destination usage bit, for a buffer something else copies into. source() is the other half.
fun source() Adds the transfer-source usage bit, for a buffer something else copies out of. dest is the other half.
operator fun invoke(buffer: BufferParameters.() -> Unit): BufferParameters Applies another block of settings and hands the same parameters back, so a prepared BufferParameters can be adjusted where it is used: params { size = 1024.floats }.
fun mergeFrom(initialDefaults: BufferParameters) Takes another request's settings as defaults under these. Its customize blocks go in front of this one's, so they run first and anything set here wins, and its allocation is adopted if this request has none of its own. This is how linkBuffer lets two boxes share one buffer.
fun incompatibilityReport(other: BufferParameters?): String A field-by-field account of why checkCompatibility said no, for the exception that reports it.
fun checkCompatibility(result: BufferParameters?): Boolean Whether an existing allocation's recorded parameters will serve for this request.
fun store(): BufferParameters A snapshot of the settled fields, recorded on every buffer this request allocates so that a later binding has something to compare itself against. It carries the flags, size, name and source block, and not the deferred customize blocks, perFrame or postAdaptation.
val buffer: FrameBufferGroup toBuffer with the renderer taken from the box context. Asking twice returns the same allocation.
fun toBuffer(renderer: Renderer = fromContext()): FrameBufferGroup Allocates the buffers this request describes, or returns the ones it already made.

vma.kt

src/jvmMain/kotlin/afield/graphics/vulkan/buffers/vma.kt — read the source

Top-level functions

Declaration Summary
fun allocatorForRenderer(d : Renderer): Long The VMA allocator for d, created on the first ask and shared by every VulkanBuffer allocated on that renderer afterwards.

Top-level values

Declaration Summary
val allocators The live VMA allocator handles, one per Renderer that has allocated a buffer.

Attachment.kt

An image plus image view sized and formatted to be used as a framebuffer attachment.

Attachment picks the view's aspect from the usage flags: a depth-stencil usage makes a depth view and sets isDepthAttachment, a colour usage makes a colour view. cleanup destroys the view and then the image.

src/jvmMain/kotlin/afield/graphics/vulkan/device/Attachment.kt — read the source

Attachment

class Attachment(device: Device, width: Int, height: Int, format: Int, usage: Int)

Declaration Summary
val image: Image
val imageView: ImageView
var isDepthAttachment: Boolean
fun cleanup()

CommandBuffer.kt

A Vulkan command buffer allocated from a CommandPool: primary or secondary, one-time-submit or reusable, with CommandBuffer.beginRecording, CommandBuffer.endRecording and CommandBuffer.reset.

It tracks whether recording is inside a render pass instance, so transfer commands are not recorded where Vulkan forbids them, and can defer host-read barriers on graphics buffers until a flush.

src/jvmMain/kotlin/afield/graphics/vulkan/device/CommandBuffer.kt — read the source

CommandBuffer

class CommandBuffer(commandPool: CommandPool, primary: Boolean, oneTimeSubmit: Boolean)

A Vulkan command buffer allocated from a CommandPool: primary or secondary, one-time-submit or reusable, with CommandBuffer.beginRecording, CommandBuffer.endRecording and CommandBuffer.reset.

Declaration Summary
var vkCommandBuffer: VkCommandBuffer The raw VkCommandBuffer handle, for the LWJGL vkCmd… calls Field has no wrapper for.
var inRenderPassInstance: Boolean True while commands are being recorded inside a render pass instance or dynamic rendering. Vulkan forbids buffer and image barriers and transfer commands there, so code that needs one tests this and defers it; whoever ends the rendering clears the flag before flushing.
data class DynamicRenderingInheritance(val colorFormats: IntArray = intArrayOf(), val depthFormat: Int = VK10.VK_FORMAT_UNDEFINED, val stencilFormat: Int = VK10.VK_FORMAT_UNDEFINED, val viewMask: Int = 0, val samples: Int = VK10.VK_SAMPLE_COUNT_1_BIT) The dynamic-rendering state a secondary buffer continues: the colour attachment formats, the depth and stencil formats, the multiview mask and the sample count. These have to match the vkCmdBeginRendering the primary buffer issued.
data class SecondaryInheritance(val renderPass: Long = 0, val subPass: Int = 0, val frameBuffer: Long = 0, val flags: Int = 0, val dynamicRendering: DynamicRenderingInheritance? = null) What a secondary buffer declares about the render pass it records into — either a render pass handle with its subpass and framebuffer, or dynamicRendering for the dynamic-rendering path. Either one makes beginRecording set the render-pass-continue flag and inRenderPassInstance.
fun beginRecording(inheritanceInfo: SecondaryInheritance? = null) Start recording, discarding anything a previous recording deferred. A secondary buffer has to pass inheritanceInfo and this throws without it; a primary buffer ignores the argument. One-time-submit is set from the flag the buffer was created with.
fun cleanup() Return the buffer to the CommandPool it was allocated from.
fun endRecording() Finish recording. Any barrier left by deferGraphicsHostReadBarrier is emitted first, unless recording is still inside a render pass instance — where emitting one is illegal, so it is discarded instead. A secondary buffer that records a deferred barrier therefore needs the primary to flush after vkCmdEndRendering.
fun reset() Clear the buffer for re-recording, releasing the memory it held back to its pool rather than keeping it for the next recording. Any deferred barrier is dropped.
fun deferGraphicsHostReadBarrier(buffer: VulkanBuffer) Ask for a graphics-write to host-read barrier on buffer, to be recorded once the render pass instance ends. This is how a shader's feedback buffer is made readable from Kotlin when the only place that knows about the read is inside the rendering, where a buffer barrier is not allowed. Repeated requests for the same buffer collapse into one.
fun flushDeferredGraphicsHostReadBarriers() Record every barrier deferGraphicsHostReadBarrier asked for as a single vkCmdPipelineBarrier2, covering each buffer's whole requested size, and empty the list. Call it after vkCmdEndRendering and after clearing inRenderPassInstance; it does nothing when nothing was deferred.

CommandPool.kt

A Vulkan command pool for one queue family, from which Field allocates command buffers.

CommandPool is created with VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, so each command buffer from it can be reset and re-recorded individually.

src/jvmMain/kotlin/afield/graphics/vulkan/device/CommandPool.kt — read the source

CommandPool

class CommandPool(device: Device, queueFamilyIndex: Int)

Declaration Summary
val device: Device
val queueFamilyIndex: Int
var vkCommandPool: Long
fun cleanup()

DescriptorSet.kt

src/jvmMain/kotlin/afield/graphics/vulkan/device/DescriptorSet.kt

DescriptorSetLayout.kt

Descriptor set layouts for the renderer's shaders: DescriptorSetLayout owns a Vulkan layout handle and releases it with its device, and DescriptorSetLayout.SimpleDescriptorSetLayout builds one from parallel lists of bindings and descriptor types.

That builder supports partially bound arrays, one variable-count (bindless) binding, and update-after-bind. UpdatesDescriptorSet is the interface a bound resource implements to push its descriptor into a command buffer.

src/jvmMain/kotlin/afield/graphics/vulkan/device/DescriptorSetLayout.kt — read the source

UpdatesDescriptorSet

interface UpdatesDescriptorSet

Descriptor set layouts for the renderer's shaders: DescriptorSetLayout owns a Vulkan layout handle and releases it with its device, and DescriptorSetLayout.SimpleDescriptorSetLayout builds one from parallel lists of bindings and descriptor types.

Declaration Summary
val binding: Introspected.Bindable
val source: BufferParameters?
fun pushDescriptorSet(command: CommandBuffer, pipelineLayout: Long, bindPoint: Int = VK_PIPELINE_BIND_POINT_GRAPHICS)

DescriptorSetLayout

abstract class DescriptorSetLayout protected constructor(private val device: Device)

Declaration Summary
var vkDescriptorLayout: Long
fun cleanup()
open class SimpleDescriptorSetLayout(val device: Device, val descriptorType: List<Int>, val set: Int, val binding: List<Int>, val stage: Int, val descriptorCount: List<Int> = List(binding.size) { 1 }, val enablePartiallyBound: Boolean = true, val variableBindingIndex: Int = -1, val enableUpdateAfterBind: Boolean = false, val immutableSamplers: Map<Int, LongArray>? = null, val pushDescriptor: Boolean = false) : DescriptorSetLayout(device) Simple layout with optional descriptor-indexing bells & whistles.

Device.kt

The logical Vulkan device: Device wraps a PhysicalDevice and enables the extensions and features the renderer uses.

It also reports optional capabilities such as ray tracing limits and Metal object export, and offers Device.calibratedHostGpuTimeNS for lining GPU timestamps up with host time.

src/jvmMain/kotlin/afield/graphics/vulkan/device/Device.kt — read the source

Device

class Device(val physicalDevice: PhysicalDevice, private val instance: Instance)

The logical Vulkan device: Device wraps a PhysicalDevice and enables the extensions and features the renderer uses.

Declaration Summary
data class RayTracingLimits(val shaderGroupHandleSize: Int, val shaderGroupHandleAlignment: Int, val shaderGroupBaseAlignment: Int, val maxRayRecursionDepth: Int, val maxRayDispatchInvocationCount: Int, val maxRayHitAttributeSize: Int, val minAccelerationStructureScratchOffsetAlignment: Int, val maxGeometryCount: Long, val maxInstanceCount: Long, val maxPrimitiveCount: Long, val maxPerStageDescriptorAccelerationStructures: Int, val maxDescriptorSetAccelerationStructures: Int) The device's ray-tracing and acceleration-structure limits, read off it once when the acceleration path is usable. Shader-binding-table layout needs the three handle sizes and alignments; geometry building needs the scratch alignment and the geometry, instance and primitive ceilings.
val openDlssNrFeatureFailure: String? Why DLSS ray reconstruction could not be prepared on this device, or null when it was. The extensions it asked for are already in enabledExtensions by then, so this is the message to show rather than a reason to re-probe.
val vkDevice: VkDevice The raw VkDevice handle, which every LWJGL call in the engine takes as its first argument.
val supportsPushDescriptors: Boolean VK_KHR_push_descriptor is available. A descriptor set layout built for push descriptors requires this, and refuses by name without it rather than failing inside Vulkan.
val pipelineStatisticsQueryEnabled: Boolean The query features GPU profiling can use, enabled whenever the device has them so that Options.profiling can be switched on a running renderer. Performance counters cost something to enable, so they are only there when profiling was on at startup.
val performanceQueryEnabled: Boolean VK_KHR_performance_query is available and profiling was on at startup. Unlike pipelineStatisticsQueryEnabled this cannot be switched later, because the counters cost something to enable. The renderer reads it together with a Windows-only test.
val pushDescriptorMax: Int The device's maxPushDescriptors, or 0 when push descriptors are unavailable. A layout asking for more than this is refused with both numbers named.
val supportsExtendedDynamicState3: Boolean VK_EXT_extended_dynamic_state3 is available and offers dynamic depth-clamp. Field sets only that one EDS3 state, so that one feature decides support; polygon mode is asked for separately where offered, since KosmicKrisp has EDS3 without it and requesting it anyway fails device creation.
val supportsExtendedDynamicState: Boolean VK_EXT_extended_dynamic_state is available. Unlike EDS3 this needs no feature beyond the extension.
val supportsMaintenance7: Boolean VK_KHR_maintenance7 is available and its feature enabled. Secondary graphics command buffers are used only where this and the option asking for them both hold.
val supportsMaintenance11: Boolean VK_KHR_maintenance11 is available and its feature enabled. It is what lets a single array layer of an image be aliased as its own descriptor.
val supportsDynamicRenderingUnusedAttachments: Boolean VK_EXT_dynamic_rendering_unused_attachments is available and its feature enabled, which is also the condition under which Field asks for it. Nothing in the engine branches on it: it relaxes which attachment formats a pipeline may declare rather than adding a call to make.
val supportsUnifiedImageLayouts: Boolean The device offers unifiedImageLayouts; usesUnifiedImageLayouts says whether Field turned it on.
val usesUnifiedImageLayouts: Boolean VK_KHR_unified_image_layouts is enabled and ImageLayouts sends GENERAL.
val supportsMultiview: Boolean VK_KHR_multiview is available, which is how one draw reaches both eyes. A render-to-texture given a multiview view mask requires it, as does the multiview side-by-side stereo target; both refuse by name without it.
val raySupport: RayDeviceSupport What the hardware offers for ray tracing, before any of Field's own opt-in. The supports… flags beside this are this probe and the ray-tracing option together.
val enabledExtensions: Set<String> Exactly the extensions vkCreateDevice was given, deduplicated. This is what Field asked for; PhysicalDevice.hasDeviceExtension is what the device offers.
val supportsAccelerationStructures: Boolean Acceleration structures can be built: ray tracing is opted into and the hardware path is complete.
val supportsRayTracing: Boolean Ray-tracing pipelines — raygen, hit and miss stages — are available and opted into.
val supportsRayQuery: Boolean Ray queries from an ordinary compute or fragment shader are available and opted into.
val rayTracingLimits: RayTracingLimits? The device's ray-tracing limits, or null when the acceleration path is unusable.
val supportsMultiviewMeshShader: Boolean Mesh-stage multiview is a separate feature from ordinary multiview.
data class MeshShaderLimits(val outputVertices: Int, val outputPrimitives: Int, val taskPayloadBytes: Int, val multiviewViews: Int, val meshInvocations: Int, val taskInvocations: Int)
val meshShaderLimits: MeshShaderLimits?
val supportsOpticalFlow: Boolean Optional shader/vision features. SDKs are not required to enable these Vulkan features.
val supportsCooperativeVector: Boolean In-shader cooperative vectors, which learned shaders infer with. It takes three features together — cooperative vectors, replicated composites and the Vulkan memory model — and can be turned off with the system property afield.cooperativeVector.disabled.
val supportsCooperativeVectorTraining: Boolean Cooperative-vector training as well as inference. Offline shader training stays available without it.
val supportsClusterAccelerationStructure: Boolean VK_NV_cluster_acceleration_structure, which needs acceleration structures underneath it. Switched off by the system property afield.clusterAccelerationStructure.disabled.
var usePresentIndexing Presents are tagged with an id and waited on, which is Windows-only and needs both VK_KHR_present_id and VK_KHR_present_wait. Construction answers this for the real device, so the value here is only what holds until then.
var enableMeshShading Mesh and task shader stages are enabled, which is Windows-only and needs VK_EXT_mesh_shader. Construction overwrites the value here the same way usePresentIndexing does.
var enableRayTracing Whether ray-tracing pipelines were enabled at device creation. supportsRayTracing is the same answer as a read-only property and is what the engine reads; construction overwrites the value here as it does for usePresentIndexing.
var usesPresentTiming True when VK_EXT_present_timing was successfully enabled on the device. Never true on macOS/MoltenVK.
val supportsMetalObjects: Boolean VK_EXT_metal_objects (MoltenVK) — required to export an RTT color image's backing IOSurface.
val supportsExternalMemoryHost: Boolean VK_EXT_external_memory_host, enabled on macOS: an IOSurface's own pages imported as a VkBuffer (afield.graphics.texture.IOSurfaceHostBuffer). That is how IOSurfaces reach Vulkan on KosmicKrisp, which imports image memory only as an MTLHeap — never an IOSurface-backed MTLTexture — and has no VK_EXT_metal_objects.
val minImportedHostPointerAlignment: Long by lazy The alignment an imported host pointer and its size must have (16 KiB on Apple silicon).
fun requireMetalObjects(what: String) Refuse, naming the way out, when supportsMetalObjects is false — which is the case on KosmicKrisp, the driver Field picks on Apple silicon with macOS 26 and a recent SDK. Field's two live IOSurface paths go through supportsExternalMemoryHost and work there; image-level IOSurface import and export is what needs this extension.
val supportsExternalMemoryFd: Boolean VK_KHR_external_memory_fd (Linux) — required to export an RTT color image's backing memory as an opaque fd for CUDA/NVENC import (see VulkanExternalMemory.kt / field-encode-linux).
val supportsExternalSemaphoreFd: Boolean Linux CUDA interop synchronization, negotiated independently of external memory.
val supportsDmaBufImport: Boolean Extensions needed to import Electron nativePixmap dma-bufs with explicit DRM modifiers.
val supportsExternalMemoryWin32: Boolean VK_KHR_external_memory_win32, used for duplicated Electron D3D11 NT handles.
fun waitIdle() Block until every queue on this device is idle. The blunt instrument teardown and swapchain rebuilds use; a per-queue or per-fence wait is what a frame should use.
var acceptingSubmissions False once the owning Renderer's teardown has begun (closeSubmissions); Queue.submit then refuses by name instead of queuing work against buffers that are about to be freed.
fun closeSubmissions() Refuse every later Queue.submit; taken under Queue.globalQueueLock so none is mid-flight.
fun cleanup() Close the reconstruction runtime if one was ever opened, then destroy the device. Everything allocated from the device has to be gone first; closeSubmissions and waitIdle are how a renderer gets there.
fun hasCalibrateableDomains(): Boolean True if DEVICE and CLOCK_MONOTONIC time domains are calibrateable (guarded by extension presence).
fun calibratedHostGpuTimeNS(): Pair<Long, Long> Returns (hostMonotonicNS, gpuNS). Requires VK_EXT_calibrated_timestamps. Callers should gate on hasCalibrateableDomains.

Fence.kt

A Vulkan fence the CPU waits on to learn that submitted GPU work has finished.

Fence creates the fence, optionally already signalled, and registers it with AutoDisposer under its Device, so a fence that is never cleaned up is still destroyed. fenceWait blocks without a timeout, reset re-arms it, and isSignaled polls it.

src/jvmMain/kotlin/afield/graphics/vulkan/device/Fence.kt — read the source

Fence

class Fence(private val device: afield.graphics.vulkan.device.Device, signaled: Boolean)

Declaration Summary
var vkFence: Long The raw VkFence handle, for the LWJGL calls Field has no wrapper for. cleanup zeroes it.
fun cleanup() Destroy the fence and clear vkFence. Safe to call twice: the second call finds the AutoDisposer registration already taken and returns without a second vkDestroyFence.
fun fenceWait() Block the calling thread until the fence signals, with no timeout. A fence that nothing submits work against never signals, so this hangs rather than returning an error.
fun reset() Return the fence to unsignalled, ready for the next submission to signal it.
fun isSignaled(): Boolean Whether the fence has signalled, polled without blocking.

ImageLayouts.kt

Maps Field's logical image layouts to the layout handed to Vulkan, collapsing to GENERAL when the driver supports VK_KHR_unified_image_layouts.

Every call that passes a layout to Vulkan goes through vkLayout; ImageLayouts holds the switch that Device sets.

src/jvmMain/kotlin/afield/graphics/vulkan/device/ImageLayouts.kt — read the source

Top-level functions

Declaration Summary
fun vkLayout(layout: Int): Int The layout to hand Vulkan for logical layout; see ImageLayouts.

ImageLayouts

object ImageLayouts

VK_KHR_unified_image_layouts: with it, GENERAL is as good as any specialised layout for every use, so a driver that offers it has nothing to gain from Field's per-use layouts, and a layered driver such as KosmicKrisp has work to lose (a transition it must honour for Metal).

Declaration Summary
var unified: Boolean Set by Device once unifiedImageLayouts is enabled; off with OpenXR, whose runtimes expect specific layouts on swapchain release.
fun vk(layout: Int): Int

Instance.kt

The Vulkan instance: SDK and driver selection, layers, extensions and the debug messenger.

Instance calls Options.doVulkanSDK before anything else, so the loader and driver it gets are the ones Field chose (KosmicKrisp or MoltenVK on macOS). It enables the validation layer when validation or shader debugPrintf is requested and a layer is installed, adds VK_KHR_portability_enumeration on macOS when the loader offers it, and records what it actually enabled in enabledExtensions. With VR enabled and an OpenXR runtime bootstrapped, the instance is created through OpenXR's xrCreateVulkanInstanceKHR, so the runtime can add the extensions it needs.

src/jvmMain/kotlin/afield/graphics/vulkan/device/Instance.kt — read the source

Instance

class Instance(private val validate: Boolean)

Declaration Summary
var vkInstance: VkInstance
val enabledExtensions: Set<String> Vulkan instance extensions actually enabled at creation time.
fun cleanup()
fun dumpToolingInfo(instance: VkInstance, phys: VkPhysicalDevice)

MacHdrActivator.kt

Enables Extended Dynamic Range (EDR) on the macOS CAMetalLayer that backs the GLFW/MoltenVK window. Without this call the OS compositor clamps output to SDR even when an HDR Vulkan swap-chain format (e.g. scRGB) is selected.

Implementation: the GLFW window handle is an NSWindow*. We walk the Objective-C object graph via the ObjC runtime (objc_msgSend) using LWJGL's low-level JNI call helpers, without pulling in any extra native library or annotation processor:

NSWindow → contentView (NSView) → layer (CAMetalLayer) → setWantsExtendedDynamicRangeContent: YES

All class and method lookups are done via reflection so this file compiles and loads on Windows/Linux without modification. It reaches LWJGL through five entry points: GLFWNativeCocoa.glfwGetCocoaWindow, LibObjC.sel_registerName, LibObjC.getLibrary, JNI.callPPP and JNI.callPPZV.

All ObjC messaging is wrapped in a try/catch so a failure degrades gracefully to SDR.

src/jvmMain/kotlin/afield/graphics/vulkan/device/MacHdrActivator.kt — read the source

MacHdrActivator

object MacHdrActivator

One function, activate, which SwapChain calls as it builds a swap chain on macOS with HDR on.

Declaration Summary
fun activate(windowHandle: Long)

PhysicalDevice.kt

The GPU Field renders on: PhysicalDevice.createPhysicalDevice picks one, optionally by preferred name, and the object records its properties, features, memory types, queue families, extensions and driver identity.

src/jvmMain/kotlin/afield/graphics/vulkan/device/PhysicalDevice.kt — read the source

PhysicalDevice

class PhysicalDevice private constructor(vkPhysicalDevice: VkPhysicalDevice)

The GPU Field renders on: PhysicalDevice.createPhysicalDevice picks one, optionally by preferred name, and the object records its properties, features, memory types, queue families, extensions and driver identity.

Declaration Summary
var vkMemoryProperties: VkPhysicalDeviceMemoryProperties The device's memory heaps and types, as an allocator reads them to choose a memory type.
var vkPhysicalDevice: VkPhysicalDevice The raw VkPhysicalDevice handle, for the LWJGL calls Field has no wrapper for.
var vkPhysicalDeviceFeatures: VkPhysicalDeviceFeatures The Vulkan 1.0 core feature set. Later feature structs are queried where they are needed.
var vkPhysicalDeviceProperties: VkPhysicalDeviceProperties Limits, device name, API version and driver version, queried once at construction.
var vkQueueFamilyProps: VkQueueFamilyProperties.Buffer One entry per queue family, carrying its capability flags, queue count and timestamp-valid bits. This is the list Queue.GraphicsQueue, Queue.ComputeQueue and Queue.PresentQueue search for a family to take a queue from.
var driverId: Int VkDriverId of the ICD behind this device, e.g. VK12.VK_DRIVER_ID_MOLTENVK; 0 if unknown.
var driverName: String The driver's own name for itself, which is what Field prints for the device it picked. Empty on a device reporting less than Vulkan 1.2, where the driver properties are not queried at all.
var driverInfo: String The driver's version string, in whatever form it chooses. Empty on the same devices driverName is.
val isMoltenVK: Boolean driverId is MoltenVK's. Nothing in Field reads this; the driver choice is made on driverId itself.
val isKosmicKrisp: Boolean driverId is Mesa's KosmicKrisp. Nothing in Field reads this; see isMoltenVK.
fun cleanup() Free the five native property structs. Called for every enumerated device that is not selected, as well as for the selected one when its renderer tears down.
val deviceName: String The name the device reports for itself, which is also what Options.preferredDevice is matched against when choosing between several GPUs.
fun hasDeviceExtension(name: String): Boolean Whether the device enumerated name among its extensions. This asks what the device offers, not what Field enabled; Device.enabledExtensions is the second question.
fun requireExtensionsIfProfiling(profilingEnabled: Boolean): Boolean True unless profilingEnabled and the device has no VK_KHR_performance_query, printing what is missing either way. A missing VK_EXT_host_query_reset is reported as a warning and does not fail the check. Nothing in Field calls this; Device.performanceQueryEnabled is the answer the renderer actually reads.

PipelineCache.kt

A Vulkan pipeline cache that the renderer passes to the pipelines it builds on one Device.

PipelineCache is created empty and lives only for the device's lifetime; nothing is saved to or loaded from disk.

src/jvmMain/kotlin/afield/graphics/vulkan/device/PipelineCache.kt — read the source

PipelineCache

class PipelineCache(device: Device)

Declaration Summary
val device: Device
var vkPipelineCache: Long
fun cleanup()

Queue.kt

Vulkan device queues: Queue fetches a queue from a family and submits command buffers with wait and signal semaphores and an optional fence.

The subclasses Queue.GraphicsQueue, Queue.ComputeQueue and Queue.PresentQueue choose the queue family for graphics, compute and presentation to a surface.

src/jvmMain/kotlin/afield/graphics/vulkan/device/Queue.kt — read the source

Queue

open class Queue(private val device: Device, queueFamilyIndex: Int, queueIndex: Int)

Vulkan device queues: Queue fetches a queue from a family and submits command buffers with wait and signal semaphores and an optional fence.

Declaration Summary
val queueFamilyIndex: Int The Vulkan queue family this queue was taken from. SwapChain compares it against the presentation queue's: images are shared CONCURRENT when they differ and EXCLUSIVE when every queue touching them is from one family.
var vkQueue: VkQueue The raw VkQueue handle, for the LWJGL calls Field has no wrapper for.
fun submit(commandBuffers: PointerBuffer?, waitSemaphores: LongBuffer?, dstStageMasks: IntBuffer?, signalSemaphores: LongBuffer?, fence: afield.graphics.vulkan.device.Fence?) Submit command buffers, waiting on waitSemaphores at the stages dstStageMasks names and signalling signalSemaphores when they complete. fence is optional and is what the CPU waits on to learn the work has finished. Taken under Queue.globalQueueLock, and refused outright once Device.closeSubmissions has run, so work is never queued against buffers a renderer teardown is about to free.
fun waitIdle() Block until everything already submitted to this queue has finished. Held under Queue.globalQueueLock, so a submit from another thread either lands before the wait or blocks until it returns.
class GraphicsQueue(device: Device, queueIndex: Int) A queue from the first family that offers graphics and compute. Field asks for both together so one queue can run a draw and a dispatch, and construction fails rather than falling back if no family offers both.
class ComputeQueue(device: Device, queueIndex: Int) A queue from the first family that offers compute, graphics or not. On a device whose first compute family is also its graphics family this is the same family Queue.GraphicsQueue picks.
class PresentQueue(device: Device, surface: Surface, queueIndex: Int) A queue from the first family that can present to surface. Presentation support is a property of the family and the surface together, so this is asked per window rather than read off the device.

RayDeviceSupport.kt

What the GPU supports for hardware ray tracing: RayDeviceSupport lists the acceleration-structure, ray-query and ray-tracing-pipeline extensions and features, and derives whether each path is usable.

lightingCapabilityReport turns that into a JSON report for a Device.

src/jvmMain/kotlin/afield/graphics/vulkan/device/RayDeviceSupport.kt — read the source

Top-level functions

Declaration Summary
fun Device.lightingCapabilityReport(): JSONObject JSON snapshot from the actual selected physical/logical device; safe on non-ray-query devices.

RayDeviceSupport

data class RayDeviceSupport(val accelerationExtension: Boolean, val deferredHostOperations: Boolean, val queryExtension: Boolean, val pipelineExtension: Boolean, val bufferDeviceAddress: Boolean, val accelerationStructure: Boolean, val rayQuery: Boolean, val rayTracingPipeline: Boolean)

Physical support, separate from Device's enabled features. Field already requires Vulkan 1.3, which supplies buffer-device-address, descriptor-indexing, SPIR-V 1.4 and float-control dependencies.

Declaration Summary
val accelerationExtension: Boolean VK_KHR_acceleration_structure is in the device's extension list.
val deferredHostOperations: Boolean VK_KHR_deferred_host_operations is present, which acceleration-structure builds depend on.
val queryExtension: Boolean VK_KHR_ray_query is in the device's extension list.
val pipelineExtension: Boolean VK_KHR_ray_tracing_pipeline is in the device's extension list.
val bufferDeviceAddress: Boolean The bufferDeviceAddress feature, which shader-side acceleration-structure access needs.
val accelerationStructure: Boolean The accelerationStructure feature. Only queried when accelerationExtension holds, so a device missing the extension reports this false as well.
val rayQuery: Boolean The rayQuery feature — tracing from an ordinary compute or fragment shader. Only queried when queryExtension holds.
val rayTracingPipeline: Boolean The rayTracingPipeline feature — raygen, hit and miss shader stages. Only queried when pipelineExtension holds.

Semaphore.kt

A Vulkan semaphore for GPU-to-GPU ordering between queue submissions, timeline by default.

A timeline Semaphore keeps its own counter: nextSignalValue advances it for the submission that will signal, currentValue is the value to wait on, and markExternallySignaled advances it when something outside Field's submissions (such as image acquisition) signals it. Constructed with timeline = false it is a binary semaphore, and both values are always 0.

src/jvmMain/kotlin/afield/graphics/vulkan/device/Semaphore.kt — read the source

Semaphore

class Semaphore(private val device: Device, private val timeline: Boolean = true)

Semaphore wrapper that can be either binary or timeline.

Declaration Summary
var vkSemaphore: Long
fun currentValue(): Long Value to wait on (current signaled value). For binary this is always 0.
fun nextSignalValue(): Long Value to signal next; increments internal counter for timelines, returns 0 for binary.
fun markExternallySignaled() Mark externally signaled (e.g., vkAcquireNextImageKHR).
fun cleanup()

Surface.kt

The Vulkan presentation surface for a GLFW window.

Surface asks GLFW to create the surface for a window handle on the instance behind a PhysicalDevice, and cleanup destroys it.

src/jvmMain/kotlin/afield/graphics/vulkan/device/Surface.kt — read the source

Surface

class Surface(physicalDevice: PhysicalDevice, windowHandle: Long)

Declaration Summary
var vkSurface: Long
fun cleanup()

SwapChain.kt

The window's presentation chain: SwapChain creates the swapchain images and their views for a surface, acquires the next image each frame and presents it on a Queue.PresentQueue.

The present mode follows the vsync setting, and SwapChain.presentImage returns a SwapChain.PresentResult that says whether the window needs resizing.

src/jvmMain/kotlin/afield/graphics/vulkan/device/SwapChain.kt — read the source

SwapChain

class SwapChain(device: Device, surface: Surface, window: Window, requestedImages: Int, vsync: Boolean, presentationQueue: PresentQueue, concurrentQueues: Array<Queue>?)

The window's presentation chain: SwapChain creates the swapchain images and their views for a surface, acquires the next image each frame and presents it on a Queue.PresentQueue.

Declaration Summary
val device: Device The logical device this chain's images and semaphores belong to.
val surface: Surface The window surface being presented to. One swapchain presents to exactly one surface.
val imageViews: Array<ImageView> A view per swapchain image, in the order the driver reports them. The driver decides how many images there are, so this array's length is the count that matters and numImages reads it.
var surfaceFormat: SurfaceFormat The pixel format and colour space chosen for the swapchain images.
var swapChainExtent: VkExtent2D The size of the swapchain images in pixels, as agreed with the surface at creation.
val syncSemaphoresList: Array<SyncSemaphores> One acquire and render-complete semaphore pair per swapchain image. Acquisition takes the pair at currentFrame; presentation waits on the pair belonging to the image being shown.
val supportsTransferSrc: Boolean The surface allows its images to be used as a transfer source, which is what lets a presented frame be copied out for a screenshot. Requested in the image usage flags only when true, since asking for an unsupported usage fails swapchain creation.
var vkSwapChain: Long The raw VkSwapchainKHR handle, for the LWJGL calls Field has no wrapper for.
val presentTimingEnabled: Boolean True when VK_EXT_present_timing is active on this swapchain (never on macOS/MoltenVK).
var refreshDurationNs: Long Nominal display refresh duration reported by vkGetSwapchainTimingPropertiesEXT (nanoseconds, 0 = unknown).
var currentFrame: Int Which syncSemaphoresList pair the next acquisition will use, advanced by presentImage rather than by acquisition. It counts presents modulo the image count, so it is not the image index Vulkan hands back and not a frame number.
var nextPresentId: Long The id the next present will be tagged with, counting from 1 because Vulkan reads 0 as untagged. It advances only while Device.usePresentIndexing holds, which is why the wait-for-present path has nothing to wait on before the first present.
data class PresentResult(val resize: Boolean, val presentId: Long?, val result: Int) What presentImage reports: whether the caller should rebuild the swapchain, the present id the frame was tagged with (null when present indexing is off) and the raw VkResult.
fun drainPastTimings() Harvest any completed past-presentation-timing entries from the driver's ring buffer and forward them to FramePresentationLog.
fun acquireNextImage() Take the next image to render into, returning its index, or -1 when the swapchain is out of date and the caller should rebuild it. The acquire is given 16 ms rather than forever, because a hidden Wayland surface can stop producing images indefinitely and the Runloop has other windows to serve; a timeout raises a retryable Renderer.ImageNotAvailable instead of blocking the frame.
fun cleanup() Destroy the swapchain along with every image view and semaphore pair it owns. The caller is responsible for having waited the device idle first; this issues no wait of its own.
val numImages: Int How many images the swapchain has, counted from what the driver created rather than from what was asked for. On a surface reporting no maximum the request is dropped and only the surface minimum is asked for, so this is the number to read before assuming a frame depth.
fun presentImage(queue: Queue, imageIndex: Int, frameId: Long = -1L, windowIndex: Int = 0): PresentResult Present the image at imageIndex, waiting on its render-complete semaphore, and advance currentFrame. frameId and windowIndex only label the entry written to FramePresentationLog. VK_ERROR_OUT_OF_DATE_KHR comes back as a resize; VK_SUBOPTIMAL_KHR asks for one only when the surface extent has really changed, since some drivers report suboptimal at a matching extent every frame and rebuilding on each would spin.
data class SurfaceFormat(val imageFormat: Int, val colorSpace: Int) A VkFormat and VkColorSpaceKHR pair, as offered by the surface and chosen at creation.
data class SyncSemaphores(val imgAcquisitionSemaphore: Semaphore, val renderCompleteSemaphore: Semaphore) The two binary semaphores one swapchain image needs: one the presentation engine signals when the image is ready to draw into, one the rendering signals when it is ready to show.

VulkanNames.kt

Debug names and command-buffer labels for Vulkan objects, as seen in validation messages and frame debuggers such as RenderDoc.

nameObject attaches a name to a device handle; beginLabel and endLabel bracket a region of a command buffer. All three do nothing unless validation is on (Options.validate) and their own toggle (Options.debugObjectNames, Options.debugLabels) is set, and when the driver does not expose the VK_EXT_debug_utils entry point. Labels are also skipped on macOS. Name strings are encoded once and cached, so the same name can be passed every frame without allocating.

src/jvmMain/kotlin/afield/graphics/vulkan/device/VulkanNames.kt — read the source

Top-level functions

Declaration Summary
fun Device.nameObject(handle: Long, type: Int, name: String)
fun CommandBuffer.beginLabel(name: String, rgba: FloatBuffer)
fun CommandBuffer.endLabel()

timing.kt

GPU-side profiling for the Vulkan renderer: per-frame query pools for timestamps, pipeline statistics and, where the driver offers VK_KHR_performance_query, performance counters.

ProfilingOptions switches each kind on and sizes its pool; GpuProfiler owns one set of pools per frame in flight and hands out query slots as commands are recorded.

src/jvmMain/kotlin/afield/graphics/vulkan/device/timing.kt — read the source

ProfilingOptions

data class ProfilingOptions(val enabled: Boolean = false, val timestamps: Boolean = true, val pipelineStats: Boolean = true, val perfCounters: Boolean = true, val maxTimestampsPerFrame: Int = 2048, val maxPipeStatsPerFrame: Int = 512, val maxPerfQueriesPerFrame: Int = 256)

GPU-side profiling for the Vulkan renderer: per-frame query pools for timestamps, pipeline statistics and, where the driver offers VK_KHR_performance_query, performance counters.

Declaration Summary
val enabled: Boolean
val timestamps: Boolean
val pipelineStats: Boolean
val perfCounters: Boolean
val maxTimestampsPerFrame: Int
val maxPipeStatsPerFrame: Int
val maxPerfQueriesPerFrame: Int

PipeStatIndex

object PipeStatIndex

Declaration Summary
const val IA_VERTICES
const val IA_PRIMITIVES
const val VS_INVOCATIONS
const val GS_INVOCATIONS
const val GS_PRIMITIVES
const val CLIP_INVOCATIONS
const val CLIP_PRIMITIVES
const val FS_INVOCATIONS
const val TCS_INVOCATIONS
const val TES_INVOCATIONS
const val CS_INVOCATIONS

FrameAllocator

class FrameAllocator(private val max: Int)

Declaration Summary
fun alloc(count: Int = 1): Int
fun capacity(): Int
fun reset()

GpuProfiler

class GpuProfiler(private val device: VkDevice, private val physicalDevice: VkPhysicalDevice, private val queueFamilyIndex: Int, private val framesInFlight: Int, val opts: ProfilingOptions)

Declaration Summary
val opts: ProfilingOptions
data class PerfCounterMeta(val name: String, val unit: Int, val scope: Int)
val perfMeta: List<PerfCounterMeta>
val enabled
val timestampsEnabled: Boolean
val pipeStatsEnabled: Boolean
val perfQueryEnabled: Boolean
val timestampPeriodNs: Double
fun beginFrame(frameIndex: Int)
fun endFrame(@Suppress("UNUSED_PARAMETER") frameIndex: Int)
fun writeTimestamp(cmd: VkCommandBuffer, frameIndex: Int, stageLegacy: Int): Int Backwards-compatible entry point: same signature, but uses sync2 when available.
fun writeTimestamp2(cmd: VkCommandBuffer, frameIndex: Int, stage2: Long): Int Optional: a native sync2 overload if you want callers to pass stage2 directly.
data class PipeStatScope internal constructor(val pool: Long, val index: Int)
fun beginPipelineStats(cmd: VkCommandBuffer, frameIndex: Int): PipeStatScope?
fun endPipelineStats(cmd: VkCommandBuffer, scope: PipeStatScope?)
data class PerfScope internal constructor(val index: Int)
fun beginPerfCounters(cmd: VkCommandBuffer, frameIndex: Int): PerfScope?
fun endPerfCounters(cmd: VkCommandBuffer, frameIndex: Int, scope: PerfScope?)
fun fetchTimestampNsRange(frameIndex: Int, first: Int, lastInclusive: Int): LongArray
fun fetchPipelineStats(frameIndex: Int, index: Int, numStatWords: Int = 11): LongArray
fun fetchPerfCounters(frameIndex: Int, index: Int, expectedCount: Int): LongArray
fun destroy()
fun formatPerfValue(v: Long, unit: Int): String

ImGuiPanel.kt

A world-space rectangle that shows an ImGuiRenderToTexture in VR and turns controller rays into ImGui pointer input.

Place the panel with ImGuiPanel.place or ImGuiPanel.faceToward, raycast it with ImGuiPanel.raycast or ImGuiPanel.interact, and draw its quad with the per-eye matrix from ImGuiPanel.panelMvp. afield.examples.four.ImGuiVrPanelDemo is a worked example.

src/jvmMain/kotlin/afield/graphics/vulkan/imgui/ImGuiPanel.kt — read the source

ImGuiPanel

class ImGuiPanel(val imgui: ImGuiRenderToTexture, var halfWidth: Float, var halfHeight: Float)

A world-space rectangular panel that shows an ImGuiRenderToTexture and can be raycast against.

Declaration Summary
val imgui: ImGuiRenderToTexture
var halfWidth: Float
var halfHeight: Float
val model World transform of the panel. Assign directly, or set it with place / faceToward.
data class Hit(val worldPoint: Vector3f, val u: Float, val v: Float, val onPanel: Boolean)
fun place(position: Vector3f, right: Vector3f, up: Vector3f): Matrix4f Orient the panel at position with the given right and up axes, which are the panel's left-to- right and bottom-to-top as its reader sees them. The text reads correctly from the side right × up points away from; faceToward works that out from where the eye is. right and up need not be unit length or exactly orthogonal — they are re-orthonormalized (up is rebuilt as normal × right).
fun faceToward(position: Vector3f, eye: Vector3f, worldUp: Vector3f = Vector3f(0f, 1f, 0f)): Matrix4f Place the panel at position with its front facing eye, kept upright against worldUp.
fun panelMvp(view: Matrix4f, projection: Matrix4f, dest: Matrix4f = Matrix4f()): Matrix4f Model-view-projection for drawing the quad, given an eye's view and projection.
fun raycast(origin: Vector3f, direction: Vector3f): Hit? Intersect a world ray with the panel plane. Returns null when the ray is parallel to the plane or the intersection is behind origin; otherwise a Hit whose Hit.onPanel says whether it landed inside the rectangle.
fun interact(origin: Vector3f, direction: Vector3f, pressed: Boolean): Hit? raycast plus ImGui pointer injection: when the ray lands on the panel the hit UV is injected as the mouse position (with pressed as the left button); otherwise the pointer is parked far off-panel so ImGui registers no hover. Returns the same Hit as raycast.

ImGuiRenderToTexture.kt

Draws Dear ImGui (through imgui-java) into a Field render-to-texture target, so an ImGui interface can be shown as a texture on any surface, including a panel in VR.

ImGuiRenderToTexture owns the ImGui context, the font atlas and the vertex buffers, and records ImGui's draw lists into its target each frame, honouring clip rects with a dynamic scissor. Only the font atlas is bound as a texture; other ImGui texture ids are ignored. ImGuiPanel places such a texture in world space and feeds controller rays back to it as pointer input.

src/jvmMain/kotlin/afield/graphics/vulkan/imgui/ImGuiRenderToTexture.kt — read the source

ImGuiRenderToTexture

class ImGuiRenderToTexture(renderer: Renderer = fromContext(), private val window: Window? = renderer.window, parameters: RTTParameters.() -> Unit = {}, var fontSupersample: Float = 4f, var monochromeTheme: Boolean = true, var useWindowMouseEvents: Boolean = true, var displayScale: Float = 1f, var ui: (ImGuiIO) -> Unit = {}) : RenderToTexture(renderer, parameters = { useDepth = false format = VK_FORMAT_B8G8R8A8_SRGB parameters() })

Minimal imgui-java renderer that draws Dear ImGui into a Field RenderToTexture.

Declaration Summary
var fontSupersample: Float Supersample the font atlas: scales the rasterization size and keeps on-screen size stable via FontGlobalScale.
var monochromeTheme: Boolean If true, converts the current style colors to grayscale for a monochrome look.
var useWindowMouseEvents: Boolean When true, feeds window mouse/scroll into ImGui when no pointer override is provided. Set false when you inject pointers manually (e.g., from a stereo controller).
var displayScale: Float Optional UI scale applied via DisplayFramebufferScale (acts like supersampling for layout).
var ui: (ImGuiIO) -> Unit
fun attachTo(target: RenderTarget = renderer.renderList, name: String = "imgui-rtt", blitName: String = "$name-blit"): ModulePointer Convenience: make sure this RTT is rendered every frame (as a preTarget) and blitted to the screen.
fun cleanup()
override fun prepareAdditionalResources(command: CommandBuffer, index: Int)
override fun renderAdditional(command: CommandBuffer, index: Int, stack: MemoryStack)
fun loadFontFromUrlAsync(url: String, sizePixels: Float = DEFAULT_FONT_SIZE, onFinished: (Boolean) -> Unit = {}) Download a TTF font asynchronously and use it once ready.
fun loadFontFromResource(resourcePath: String, sizePixels: Float = DEFAULT_FONT_SIZE, onFinished: (Boolean) -> Unit = {}) Load a TTF font from application resources (classpath).
fun loadFontFromBytes(bytes: ByteArray, sizePixels: Float = DEFAULT_FONT_SIZE) Load a TTF font from an in-memory byte array immediately (next frame).
fun add(name: String, block: ImGuiDsl.(ImGuiIO) -> Unit)
fun addUi(name: String, block: ImGuiDsl.() -> Unit)
fun addLegacyUi(name: String, block: (ImGuiIO) -> Unit)
fun removeUi(name: String)
fun injectPointer(x: Float, y: Float, down: Boolean, scrollY: Float = 0f) Inject a pointer in RTT pixel coordinates (origin top-left). Overrides window mouse for the next frame.
class ImGuiDsl

BinocularGazeIntersection.kt

Estimates where the two eyes converge from a pair of per-eye gaze rays.

BinocularGazeIntersection takes the closest approach of the left and right rays and rejects depths it cannot trust, so a caller can place an object at the point the viewer is looking at. BinocularGazeIntersectionTest covers it.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/BinocularGazeIntersection.kt — read the source

BinocularGazeIntersection

class BinocularGazeIntersection(val minimumAngleDegrees: Double = 0.25, val maximumSeparation: Double = 0.03, val minimumDistance: Double = 0.05, val maximumDistance: Double = 8.0)

Closest approach of two forward eye rays, in metres, with explicit rejection of unreliable depth. Real rays can be skew: position is the midpoint of leftPoint and rightPoint, and separation is their gap. Read valid before placing an object there.

Declaration Summary
val minimumAngleDegrees: Double The smallest angle between the two rays that still gives a depth, in degrees. Nearer to parallel than this the closest-approach solve is ill-conditioned, so status becomes Status.PARALLEL and no point is written. Must be above 0 and below 90.
val maximumSeparation: Double How far leftPoint and rightPoint may be apart, in metres, before the rays count as missing each other and status becomes Status.RAYS_MISS. Skew rays always leave some gap; this is how much of it to forgive.
val minimumDistance: Double Nearest accepted depth along either ray, in metres. Closer gives Status.OUT_OF_RANGE.
val maximumDistance: Double Farthest accepted depth along either ray, in metres. Beyond it, Status.OUT_OF_RANGE.
enum class Status
var status Why the last update or solve accepted or rejected a depth. Status.NO_TRACKING until the first call.
val valid: Boolean True only for Status.VALID — the one check to make before using position.
var raysValid True when both rays were finite, long enough to normalize and had distinct origins, so leftDirection and rightDirection are meaningful. The convergence tests come after this, so a pair of usable rays that converge badly leaves this true while valid is false.
val position Midpoint of leftPoint and rightPoint — where to put an object the viewer is looking at.
val leftPoint The point on the left eye's ray closest to the right eye's ray.
val rightPoint The point on the right eye's ray closest to the left eye's ray.
val leftDirection Unit forward direction of the left eye's ray, as the last solve normalized it.
val rightDirection Unit forward direction of the right eye's ray, as the last solve normalized it.
var leftDistance Distance from the left eye to leftPoint, in metres. Negative means the convergence is behind the eye.
var rightDistance Distance from the right eye to rightPoint, in metres. Negative means behind the eye.
var separation Gap between leftPoint and rightPoint, in metres — how far the two rays missed by.
var angleDegrees Angle between the two rays, in degrees. Written before the convergence tests, so it is readable even when the rays were too close to parallel to intersect.
fun invalidate()
fun update(eyes: HtcEyeTrackingState?): Boolean
fun solve(leftOrigin: Vec3, leftRay: Vec3, rightOrigin: Vec3, rightRay: Vec3): Boolean Both origins and directions must use the same coordinate system; directions may be unnormalized.

ComputeTarget.kt

A Target that records compute and ray-tracing modules into a frame without drawing to a window.

Modules and plain command-recording functions are added with += on a ComputeTarget and run each frame: compute modules first, then ray-tracing modules, then the other targets.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/ComputeTarget.kt — read the source

ComputeTarget

class ComputeTarget(val renderer: Renderer) : Target

Records named compute modules, ray-tracing modules and other Targets into the frame's command buffer.

Declaration Summary
val renderer: Renderer The renderer this target records into, and the identity each added module's lifecycle claim is filed under.
var modules The compute modules dispatched each frame, by name, in insertion order and before rayTracingModules. Add with +=.
var rayTracingModules The ray-tracing modules traced each frame, by name, after every compute module has been recorded.
var others Targets recorded after this one's own modules, by name. A (name, (CommandBuffer, Int) -> Unit) pair added with += lands here too, wrapped as a target, which is how plain recording code joins the frame.
operator fun plusAssign(m: Pair<String, (CommandBuffer, Int) -> Unit>)+6 overloads · also Pair<String, ComputeModulePointer>, Named<ComputeModulePointer>, Pair<String, ComputeModule>, Pair<String, RayTracingModulePointer>, Pair<String, RayTracingModule>, Pair<String, Target>
override fun render(command: CommandBuffer, index: Int)

ExtEyeGazeInteraction.kt

Combined eye gaze from OpenXR's XR_EXT_eye_gaze_interaction, as a single pose action inside the HMD target's existing gameplay action set.

Creation returns null on a runtime that does not advertise the extension, reports no eye-gaze support in its system properties, or refuses the action, its suggested binding or its action space, so a target asks once and then has either gaze or none. It has to happen before the action sets are attached to the session. Each frame the gaze pose is located against the target's reference space and written into an EyeGazeState with the sample time the runtime reports for it; the state is invalidated first, so a frame in which the action is not active reads as inactive rather than as the last pose that worked.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/ExtEyeGazeInteraction.kt — read the source

FrameHistory.kt

Tracks the previous frame's camera for one render target, so temporal passes can reproject pixels and know when there is no usable history.

FrameHistory holds the reprojection matrix and an availability flag that drops whenever the frame sequence, size or scene epoch breaks.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/FrameHistory.kt — read the source

FrameHistory

class FrameHistory

One render target's frame history: where the camera was last frame, and whether that answer is usable.

Declaration Summary
val current This frame's view-projection.
val previous Last frame's view-projection, or a copy of current when available is false.
val inverse inverse(current) — current clip to world, for a pass that unprojects.
val reprojection previous · inverse(current) — current clip position to previous clip position.
var available True if previous describes the immediately preceding frame of the same target and scene.
var frameId The frame id of the last capture, or Long.MIN_VALUE before the first one.
var width Width at the last capture.
var height Height at the last capture.
val serial: Long Alias for frameId, matching the private prototype this was generalised from.
val packed Four matrices then four scalars, ready for a uniform buffer.
fun invalidate() Drop the history at the next capture.
fun capture(frameId: Long, viewProjection: Matrix4fc, width: Int, height: Int, epoch: Any): Boolean Advance to frameId, and report whether history is now available.
fun params(): Vector4f (width, height, available, 0) — the companion vec4 to reprojection in a push constant.
fun params(out: Vector4f): Vector4f Reuse a vector when polling every frame.
override fun toString(): String

HasRenderToTexture.kt

A RenderToTexture owner that modules can be attached to as if it were the target itself.

ElectronRTTView implements HasRenderToTexture over the target it draws into, and WebRTCSink forwards to the view it captures from, so a module reaches either of them the two ways it reaches a RenderToTexture: +view inside moduleHere { }, or view += "name" to module. The second form is the two operators here; the first is an operator on Parameters, the moduleHere { } receiver.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/HasRenderToTexture.kt — read the source

Top-level functions

Declaration Summary
operator fun HasRenderToTexture.plusAssign(m: Pair<String, ModulePointer>)+1 overload · also Pair<String, Module> Attaches a module to HasRenderToTexture.rtt, as rtt += "name" to module does.

HasRenderToTexture

interface HasRenderToTexture

Something that draws through a RenderToTexture it owns, such as ElectronRTTView, and can stand in for it: +view inside moduleHere { } points the module at rtt, and view += "name" to module attaches the module to it.

Declaration Summary
val rtt: RenderToTexture The target this object draws through.

HtcEyeTracker.kt

Native binding for XR_HTC_eye_tracker, which Field's LWJGL version does not generate.

Layouts and signatures follow HTC's ViveEyeTrackerHelper.cs and enum values in OpenXRUtils.cs: https://github.com/ViveSoftware/VIVE-OpenXR-Unity/tree/25a5fd212420688952ead9deba735357656278ec/com.htc.upm.vive.openxr/Runtime

Only 64-bit ABIs are supported: enum/bool32/float = 4 bytes, pointer/handle/time = 8 bytes, and create returns null on any other pointer width. The per-eye arrays are inline rather than pointers, and their elements carry no type/next header.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/HtcEyeTracker.kt — read the source

MndxXDevSpace.kt

Hand-rolled bindings for XR_MNDX_xdev_space.

This is a Monado vendor extension: it never made it into the official OpenXR registry, so LWJGL generates no bindings for it. We resolve the entry points through xrGetInstanceProcAddr and lay the structs out by hand. Definitions transcribed from Monado's src/external/openxr_includes/openxr/XR_MNDX_xdev_space.h (spec version 3, extension 445).

It is the only route to WiVRn's body trackers. The obvious one, XR_FB_body_tracking, is a dead end there; OpenXRHmdTarget's body-tracking setup says why, and cites the WiVRn source.

The extension enumerates every "xdev" (tracked device) the runtime knows about (HMD, controllers, body trackers) and hands back an XrSpace per device, which the caller then locates with the ordinary xrLocateSpace.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/MndxXDevSpace.kt — read the source

MotionItems.kt

Per-item motion for 2D items drawn by stable identity, for passes that need each item's previous pose.

MotionItems keeps this frame's and last frame's MotionPose for each key and writes the difference, falling back to no motion when the item is new, changed revision or was cleared. MotionItemsTest covers it.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/MotionItems.kt — read the source

MotionPose

class MotionPose(var x: Float = 0f, var y: Float = 0f, var angle: Float = 0f, var scale: Float = 1f, var energy: Float = 1f, var softness: Float = 0f)

Mutable 2D affine pose. Angles are radians; scale is positive and uniform.

Declaration Summary
var x: Float Centre of the item on the x axis, in whatever units the pass draws in.
var y: Float Centre of the item on the y axis.
var angle: Float Rotation about that centre, in radians. Only the difference between two frames is written out.
var scale: Float Uniform scale about that centre. Must be finite and above zero; MotionItems.write requires it.
var energy: Float How much this item should count for. MotionItems.write treats a previous energy at or below 1e-7 as no usable history, so a fading item stops contributing motion before it disappears.
var softness: Float How soft the item's edge is. Like energy, what reaches the pass is the previous frame's value.
fun set(other: MotionPose)

MotionItems

class MotionItems

Correspondence by stable item identity, not draw-list index. Call begin once per rendered frame (duplicate calls are harmless). Topology or material changes use a different revision; camera cuts and world resets call clear. Retains only this frame and its immediate predecessor. Owner-thread only.

Declaration Summary
val retained: Int How many keys are being held, which is this frame's items plus any from the frame before that have not been written again yet. begin drops the rest, and clear takes it to zero.
fun begin(serial: Long)
fun write(key: Any, pose: MotionPose, out: FloatArray, at: Int = 0, allow: Boolean = true, revision: Any? = null) Writes two vec4s: previous centre xy, cosine/sine scaled by previous/current scale; then valid, previous energy, previous softness, reserved. Use FrameHistory.available as allow when camera history is required.
fun clear()

MultiPingPongRenderToTexture.kt

Several passes per frame between two images: MultiPingPongRenderToTexture runs an optional init pass, a number of middle passes and an optional final pass, each reading what the last one wrote.

It suits iterated kernels such as blurs or relaxation steps, where one frame needs many passes. Shaders can tell the passes apart through push constants the target fills in (pass index, pass count, and whether this is the init or final pass).

src/jvmMain/kotlin/afield/graphics/vulkan/targets/MultiPingPongRenderToTexture.kt — read the source

MultiPingPongRenderToTexture

class MultiPingPongRenderToTexture(val renderer: Renderer = fromContext(), configure: RTTParameters.() -> Unit = {}) : Target, HasImageView, HasImageViewArray, HasDefaultTextureSampler

Multi-pass ping-pong render target.

Declaration Summary
val renderer: Renderer The renderer the two internal targets are built on.
enum class PassKind
var parameters The RTTParameters both internal targets were built from. One color attachment and no external views, which the constructor requires; depth is forced off, and the size, format, mip and clearing fields are copied into each target when it is created, so editing them afterwards does not reach the images.
override var defaultSampler: TextureSampler?
fun setDefaultSampler(anisotropyEnable: Boolean = true, filter: Int = VK10.VK_FILTER_LINEAR, mipmapMode: Int = if (filter == VK10.VK_FILTER_LINEAR) VK10.VK_SAMPLER_MIPMAP_MODE_LINEAR else VK10.VK_SAMPLER_MIPMAP_MODE_NEAREST, addressModeU: Int = VK10.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, addressModeV: Int = VK10.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, addressModeW: Int = VK10.VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE): TextureSampler
fun repeatU(anisotropyEnable: Boolean = true, filter: Int = VK10.VK_FILTER_LINEAR, mipmapMode: Int = if (filter == VK10.VK_FILTER_LINEAR) VK10.VK_SAMPLER_MIPMAP_MODE_LINEAR else VK10.VK_SAMPLER_MIPMAP_MODE_NEAREST): TextureSampler
var currentPassIndex: Int Which pass is recording, counted from 0 over every pass in the frame, and -1 between frames and whenever the frame has no passes at all. The same number the shaders read as passIndex.
var currentPassKind: PassKind Which phase is recording: the init pass, one of the middle passes, or the final one, and PassKind.NONE outside a pass.
var otherPassCount: Int How many middle passes to run each frame, between the init and final ones. This is the dial an iterated kernel turns: four blur passes, or forty relaxation steps. A negative value is read as none.
var runInitPass: Boolean Whether to run the init pass, the one that seeds the image the middle passes iterate.
var runFinalPass: Boolean Whether to run the final pass, the one that reads the last middle pass and writes the image this target then publishes as its own view.
val initModules The modules the init pass draws, by name. Added with addInit.
val otherPassModules The modules every middle pass draws, by name: the same modules re-recorded once per pass, reading what the pass before them wrote. Added with +=, or with addOther.
val finalModules The modules the final pass draws, by name. Added with addFinal.
val inputImageView: HasImageView The image the recording pass reads: whichever of the two targets the pass before it wrote. Bind this where a module samples the previous pass's output; the object is fixed and its view follows the swap.
val inputImageViewArray: HasImageViewArray inputImageView as a one-element array, for a binding that takes an image array rather than a single image.
val writeImageView: HasImageView The image the recording pass is writing — the other target from inputImageView. Reading it while that pass runs is reading an attachment the pass has not finished, so the frame's result is the view this target itself publishes.
override val imageView: ImageView
override val imageViewArray: List<HasImageView>
fun blitToScreen(renderer: Renderer = this.renderer, target: RenderTarget = renderer.renderList, attachment: Int = 0, name: String = "rtt-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: RenderToTexture.ColorGrade.() -> Unit = {}): ModulePointer Matches RenderToTexture.blitToScreen, delegating to whichever internal target contains the final output for the configured pass count parity.
fun blitToScreen(windowIndex: Int, renderer: Renderer = this.renderer, attachment: Int = 0, name: String = "rtt-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: RenderToTexture.ColorGrade.() -> Unit = {}): ModulePointer
fun blitToScreen(window: Window, renderer: Renderer = this.renderer, attachment: Int = 0, name: String = "rtt-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: RenderToTexture.ColorGrade.() -> Unit = {}): ModulePointer
fun debugBlitToScreen(renderer: Renderer = this.renderer, target: RenderTarget = renderer.renderList, attachment: Int = 0, name: String = "rtt-debug-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: RenderToTexture.ColorGrade.() -> Unit = {}): ModulePointer
fun debugBlitToScreen(windowIndex: Int, renderer: Renderer = this.renderer, attachment: Int = 0, name: String = "rtt-debug-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: RenderToTexture.ColorGrade.() -> Unit = {}): ModulePointer
fun debugBlitToScreen(window: Window, renderer: Renderer = this.renderer, attachment: Int = 0, name: String = "rtt-debug-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: RenderToTexture.ColorGrade.() -> Unit = {}): ModulePointer
fun addInit(name: String, module: ModulePointer)+1 overload · also Module
fun addOther(name: String, module: ModulePointer)+1 overload · also Module
fun addFinal(name: String, module: ModulePointer)+1 overload · also Module
operator fun plusAssign(m: Pair<String, ModulePointer>)+1 overload · also Pair<String, Module>
override fun render(command: CommandBuffer, index: Int)
override fun toString(): String

MultiviewStereoTarget.kt

src/jvmMain/kotlin/afield/graphics/vulkan/targets/MultiviewStereoTarget.kt — read the source

MultiviewStereoTarget

class MultiviewStereoTarget(val renderer: Renderer = fromContext(), val eyeWidth: Int = renderer.window.width / 2, val eyeHeight: Int = renderer.window.height, val openXR: OpenXRHmdTarget? = null) : Target, HasImageView

One scene pass into two array layers, with one shared per-frame eye UBO. No packed intermediate.

Declaration Summary
val renderer: Renderer
val eyeWidth: Int
val eyeHeight: Int
val openXR: OpenXRHmdTarget?
val stereoCamera
val layered
override val imageView
fun configure(p: Parameters) Apply the layered pass settings and bind this target's single shared eye buffer.
fun registerModule(name: String, factory: () -> ModulePointer)
fun registerModule(name: String, module: ModulePointer)
operator fun plusAssign(entry: Pair<String, ModulePointer>)
fun eyeViewMatrix(eye: Int, out: Matrix4f = Matrix4f()): Matrix4f
fun eyeProjectionMatrix(eye: Int, out: Matrix4f = Matrix4f()): Matrix4f
val beforeScene GPU work that needs the current predicted eyes, recorded once before the stereo scene.
override fun render(command: CommandBuffer, index: Int)
fun mirrorToScreen(windowIndex: Int = 0, name: String = "stereo-mirror", stereoPair: () -> Boolean = { false }): ModulePointer A read-only desktop view of the actual array image, optionally displaying both layers.
fun controllerState(hand: Hand = Hand.RIGHT, out: ControllerState = ControllerState()): ControllerState
fun updateMockControllerFromCamera(distanceMeters: Float = 1f)
fun updateMockControllerFromRay(origin: Vector3f, direction: Vector3f, distanceMeters: Float = 1f)
fun enableMouseController(mouse: MouseEvents = renderer.window.mouseEvents ?: MouseEvents(renderer), name: String = "sbs-multiview-mouse-controller", distanceMeters: Float = 1f): () -> Unit

OpenXREyeTracking.kt

Plain state holders for OpenXR eye tracking: the combined gaze of XR_EXT_eye_gaze_interaction and the per-eye gaze, pupil and geometry streams of XR_HTC_eye_tracker.

Each holder is filled in place on the renderer's queue thread and carries its own validity flags, so read EyeGazeState.poseValid or the per-stream flags of HtcEyeState before using a value.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/OpenXREyeTracking.kt — read the source

EyeGazeState

class EyeGazeState

Combined gaze from XR_EXT_eye_gaze_interaction, in the target's LOCAL reference space (metres, +Y up, -Z forward). Updated in place on the renderer's queue thread. Numeric pose values retain the last valid observation; check poseValid before using them.

Declaration Summary
val position Where the combined gaze ray starts, in LOCAL space, in metres. Only written while positionValid.
val orientation Which way the gaze points, as a rotation of local -Z. Only written while orientationValid.
var isActive False when the runtime reported no gaze action at all this frame, so nothing else here is current.
var positionValid The runtime gave a usable position in the last frame it was asked.
var orientationValid The runtime gave a usable orientation in the last frame it was asked.
var positionTracked The position is being actively tracked rather than inferred. Valid but untracked is the runtime's best guess from an older observation, which is usable and should be trusted less.
var orientationTracked The orientation is being actively tracked rather than inferred — see positionTracked.
var sampleTime: Long OpenXR time in nanoseconds at which the pose is expressed; zero when unavailable.
val poseValid: Boolean isActive with both halves of the pose valid — the one check to make before reading the pose.
val isTracked: Boolean False for a valid but degraded/untracked pose.
fun direction(out: Vec3): Boolean Copies the gaze's forward (-Z) direction into out, only when the whole pose is valid.

HtcEyeState

class HtcEyeState

One eye's independent observations from XR_HTC_eye_tracker. Check each validity flag.

Declaration Summary
val position Gaze pose in the target's LOCAL reference space, in metres. Forward is local -Z.
val orientation Which way this eye points, as a rotation of local -Z. Written together with position, under gazeValid.
var gazeValid This eye's position and orientation came through in the last gaze query.
var pupilDiameterValid pupilDiameter came through in the last pupil query. Independent of pupilPositionValid.
var pupilPositionValid pupilPosition came through in the last pupil query. Independent of pupilDiameterValid.
var pupilDiameter Pupil diameter in millimetres (unlike the gaze position).
val pupilPosition Normalized sensor coordinates [0, 1], +X right, +Y up.
var geometricValid eyeOpenness, eyeWide and eyeSqueeze came through in the last geometry query, as one group.
var eyeOpenness How open the eye is, 0 shut to 1 open. Written under geometricValid.
var eyeWide How wide the eye is held open past its resting opening, 0 to 1. Written under geometricValid.
var eyeSqueeze How tightly the eye is squeezed shut, 0 to 1. Written under geometricValid.

HtcEyeTrackingState

class HtcEyeTrackingState

HTC per-eye gaze, pupil and geometry observations, updated in place on the renderer's queue thread. The three streams have independent validity and OpenXR timestamps (nanoseconds). A failed stream does not suppress the other streams. Numeric values retain the last valid observation; timestamps reset to zero when a query fails or a frame is skipped.

Declaration Summary
val left The left eye's observations. Each of its three streams carries its own validity flag.
val right The right eye's observations, read from the same three queries as left.
var gazeTime: Long OpenXR time in nanoseconds of the gaze poses in left and right; zero when that query failed.
var pupilTime: Long OpenXR time in nanoseconds of the pupil diameters and positions; zero when that query failed.
var geometricTime: Long OpenXR time in nanoseconds of the openness, wide and squeeze values; zero when that query failed.

OpenXRFrame.kt

src/jvmMain/kotlin/afield/graphics/vulkan/targets/OpenXRFrame.kt — read the source

OpenXRHmdTarget.kt

The OpenXR headset as a render target: OpenXRHmdTarget acquires each eye's swapchain image, renders registered modules into it, and submits the frame with a projection layer.

It uses the OpenXR session that the Vulkan instance creates when VR is enabled, and also reports head and controller poses. Instead of rendering per eye, it can copy a side-by-side texture (from SideBySideStereoTarget or a wide RenderToTexture) into the two eyes.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/OpenXRHmdTarget.kt — read the source

OpenXRHmdTarget

class OpenXRHmdTarget(val renderer: Renderer = fromContext())

OpenXR HMD render target.

Declaration Summary
val renderer: Renderer The renderer whose Vulkan device the OpenXR session and its swapchains are created against, on that renderer's queue thread, at the first renderFrame. Found in the box graph by default. The XrInstance itself is older than any device and comes from the bootstrap afield.graphics.vulkan.device.Instance made at startup, which construction claims here.
var currentEyeIndex: Int Index of the eye currently being rendered inside renderEyesVulkan, or -1 when idle.
val hasEyeViews: Boolean True once the runtime has allocated a complete stereo view/projection pair.
fun currentEyeViewMatrix(out: Matrix4f = Matrix4f()): Matrix4f Copies the current eye's view matrix into out and returns it. Only valid while rendering (i.e. called from module render callbacks).
fun currentEyeProjectionMatrix(out: Matrix4f = Matrix4f()): Matrix4f Copies the current eye's projection matrix into out and returns it. Only valid while rendering (i.e. called from module render callbacks).
fun eyeViewMatrix(eyeIndex: Int, out: Matrix4f = Matrix4f()): Matrix4f Like currentEyeViewMatrix but for an explicit eye index.
fun eyeProjectionMatrix(eyeIndex: Int, out: Matrix4f = Matrix4f()): Matrix4f Like currentEyeProjectionMatrix but for an explicit eye index.
fun headPose(outPosition: Vec3 = Vec3(), outOrientation: Quat = Quat()): Boolean Approximate head pose from the currently tracked eye views.
enum class Hand
data class JointPose(var position: Vec3 = Vec3(), var orientation: Quat = Quat(), var radius: Float = 0f, var positionValid: Boolean = false, var orientationValid: Boolean = false)
enum class HandJoint
class HandTrackingState
enum class BodyJoint The body joints reachable over WiVRn, in the order its client forwards them.
class BodyTrackingState
data class ControllerState(var active: Boolean = false, var poseValid: Boolean = false, var position : Vec3 = Vec3(), var rotation : Quat = Quat(), var selectPressed: Boolean = false, var triggerValue: Float = 0f, var triggerPressed: Boolean = false, var squeezeValue: Float = 0f, var squeezePressed: Boolean = false, var trackpadX: Float = 0f, var trackpadY: Float = 0f, var trackpadTouched: Boolean = false, var trackpadPressed: Boolean = false, var thumbstickX: Float = 0f, var thumbstickY: Float = 0f, var thumbstickTouched: Boolean = false, var thumbstickPressed: Boolean = false, var aPressed: Boolean = false, var bPressed: Boolean = false, var menuPressed: Boolean = false)
fun eyeGazeState(): EyeGazeState? Combined gaze from XR_EXT_eye_gaze_interaction, or null when unavailable/not initialized. The state is updated in place; check poseValid (and isTracked for nominal-quality gaze).
fun htcEyeState(): HtcEyeTrackingState? Per-eye gaze, pupil and geometry from XR_HTC_eye_tracker, or null when unavailable. Independent of eyeGazeState; check each observation's validity before using its values.
fun handState(hand: Hand): HandTrackingState?
fun bodyState(): BodyTrackingState? Null when the runtime exposes no body trackers at all.
fun bodyTrackerCount(): Int How many raw body trackers the runtime is publishing.
fun bodyTrackerPose(index: Int): JointPose? The raw pose of the n-th tracker, bypassing the semantic mapping. Use this when the joint mapping came out empty — a runtime other than WiVRn, or a tracker count we don't recognise — and you would still rather have the poses than nothing.
fun controllerState(hand: Hand, out: ControllerState = ControllerState()): ControllerState
fun hapticPulse(hand: Hand, amplitude: Float = 0.5f, durationNs: Long = 20_000_000L, frequencyHz: Float = 0f)
fun controllerGripPoseMatrix(hand: Hand, out: Matrix4f = Matrix4f()): Matrix4f
var modules The graphics modules to draw, by name, the same way RenderTarget holds them. Every module here is rendered once per eye; adding a name that is already present replaces it.
var meshModules The mesh-shader modules to draw, by name, rendered per eye alongside modules.
var clearColor: FloatArray What each eye's swapchain image is cleared to, as RGBA, copied from Options.openXRClearColor at construction. An array of fewer than four is read channel by channel, with the missing channels taken from opaque magenta (1, 0, 1, 1) rather than from zero.
fun setClearColor(r: Float, g: Float, b: Float, a: Float = 1f)
fun sideBySideRTTParameters(parameters: RTTParameters.() -> Unit = {}): RTTParameters Convenience: Build an RTTParameters for a single side-by-side render target (left eye on the left half, right eye on the right half) matching the OpenXR recommended eye dimensions.
fun copySideBySideToEyes(source: HasImageView) Convenience: Copy a side-by-side image into the OpenXR eyes every frame.
fun copyLayersToEyes(source: HasImageView) Copy array layer zero/one directly into the acquired left/right OpenXR images.
fun clearEyeSource(source: HasImageView)
fun disableSideBySideCopy()
operator fun plusAssign(m: Pair<String, MeshModulePointer>)+5 overloads · also Pair<String, ModulePointer>, ModulePointer, Pair<String, Module>, MeshModulePointer, Pair<String, MeshModule>
data class XrFrameMarkers(var tsFrameBegin: Int = -1, var tsAfterEyes: Int = -1, var tsFrameEnd: Int = -1, var pipeStatsEyes: Int = -1, var perfFrameIndex: Int = -1)
data class XrEyeMarks(val eyeIndex: Int, var tsBegin: Int = -1, var tsEnd: Int = -1, var pipeStatsIndex: Int = -1, var perfIndex: Int = -1)
data class XrProfilerState(val profiler: GpuProfiler, var markers: XrFrameMarkers = XrFrameMarkers(), var perFrameEyeMarks: MutableList<XrEyeMarks> = mutableListOf(), val pending: ArrayDeque<Pending> = ArrayDeque(), val lock: Any = Any())
data class Pending(val frameCounter: Long)
var xrFrameCounter How many OpenXR frames this target has worked through, counted at the end of renderFrame's queue-thread pass rather than by the renderer, so it does not follow Runloop.frame and does not advance while the target is disabled. It is the frame identity the GPU profiler pairs its readbacks with, and what the XR CPU-span report prints against.
fun renderFrame(renderScene: () -> Unit = {}) Wait and locate this frame before renderScene records desktop/stereo work, then submit OpenXR using exactly those views and that predicted display time. The scene callback runs on the caller thread, outside the queue executor, so graphics workers can submit safely. It also runs when XR is inactive, keeping the desktop usable while a headset is asleep.
var locatedViewFrameId: Long Frame whose views scene render callbacks currently read; -1 before the first locate.
var locatedViewDisplayTime: Long Predicted display time, in the runtime's clock, shared by scene rendering and submission.
fun cleanup()

OwnedModules.kt

Ownership for the modules a box puts into a renderer's target maps, which is what renderer += "name" to module records so that something can take the module out again.

The map belongs to the renderer and the module to the box that compiled it, a split the map alone cannot express: a deleted box's module would otherwise go on drawing, holding the snippet classloader that built it. Each put therefore files a lifecycle claim under afield.runtime.lifecycle.ReleasePolicy.KEYED, so stopping the box leaves the module drawing, a re-run under the same name replaces it, and retiring the box (delete, document close) removes it. The claim is filed under the renderer as well, and disposing the renderer ends the ones whose device has gone.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/OwnedModules.kt — read the source

PingPongRenderToTexture.kt

Feedback rendering: PingPongRenderToTexture keeps a ring of images per color attachment, so a module can sample the previous frame while drawing the next.

Each frame renders into one slot of the ring and exposes the earlier, completed slots as textures, which is the shape of trails, reaction-diffusion and other effects that read their own last output. FrameCadencedPingPongRenderToTexture advances the ring only every few frames, for a slow sequence that downstream code crossfades between.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/PingPongRenderToTexture.kt — read the source

Used in: Field 4 Graphics

PingPongRenderToTexture

open class PingPongRenderToTexture(renderer: Renderer = fromContext(), frameCount: Int = 2, parameters: RTTParameters.() -> Unit = {}) : RenderToTexture(renderer, parameters)

RenderToTexture variant that ring-buffers every color attachment. Each frame it renders into one image set while exposing previous completed frames via otherImageView and previousFrameImageViews, so modules can sample feedback history without managing several RTT instances.

Declaration Summary
val frameCount: Int How many images the ring holds per color attachment, and so how far back previousFrameImageViews reaches: a frame writes one slot and the other frameCount - 1 stay readable. Two or more, and two is the plain feedback case.
open val otherImageView: HasImageView
open val thisImageView: HasImageView
val latestImageView: HasImageView Color attachment 0 of the slot the last completed frame wrote. The object is fixed and its view follows the ring, so a binding made once keeps reading the latest frame; while a pass records, that is the same image otherImageView gives.
val previousImageView: HasImageView Color attachment 0 of the slot written one frame before latestImageView's, which is two frames back from the one being written.
val colorAttachmentCount: Int How many color attachments the ring carries, each with its own frameCount images. latestImageViewFor and the other per-attachment accessors take a slot in 0 until colorAttachmentCount.
fun recordReplaceHistory(command: CommandBuffer, attachment: Int = 0, copy: (vkImage: Long) -> Unit) Replace every history slot with one coherent external state, without rotating the ring. Intended for prepared playback cuts: copy records a GPU transfer into each image. The ordinary RTT transition helper owns the layouts, so subsequent feedback cannot read stale pre-cut history. Requires colorTransferDst = true, colorSampled = true and a single mip level. Call outside a render pass on the graphics recording thread. The callback records a copy of mip 0, including every array layer; it must not alias any ring image. Ring indices, cadence and the currently bound attachment remain unchanged. This records commands only; Renderer.whenCurrentGraphicsFrameComplete can report their completion.
open fun otherImageViewFor(attachment: Int): HasImageView
open fun thisImageViewFor(attachment: Int): HasImageView
fun latestImageViewFor(attachment: Int): HasImageView
fun previousImageViewFor(attachment: Int): HasImageView
val previousFrameImageViews: HasImageViewArray Previous completed frames in render-time order: index 0 is the most recent completed frame, index 1 is the frame before that, and so on.
fun previousFrameImageViewsFor(attachment: Int): HasImageViewArray
val frameImageViews: HasImageViewArray All frame slots in write order, starting with writeIndex. Outside the update render this is oldest-to-newest for a warmed-up ring, with thisImageView substituted at its physical slot.
fun frameImageViewsFor(attachment: Int): HasImageViewArray
val writeFrameImageViews: HasImageViewArray Explicit alias for frameImageViews, named for code that wants to state that it depends on the write-order rotation.
fun writeFrameImageViewsFor(attachment: Int): HasImageViewArray
open fun frameImageView(offset: Int): HasImageView Returns a frame by this target's default binding order.
open fun frameImageView(offset: Int, attachment: Int): HasImageView
fun writeFrameImageView(offset: Int): HasImageView Returns a frame by write-order offset. Offset 0 is writeIndex, offset 1 is the next physical ring slot, and so on. When the selected physical slot is the slot represented by thisImageView, it resolves through thisImageView so subclass dynamic semantics are preserved.
fun writeFrameImageView(offset: Int, attachment: Int): HasImageView
fun previousFrameImageView(age: Int): HasImageView Returns the Nth previous completed frame relative to the next/current render. age == 1 is the most recent completed frame.
fun previousFrameImageView(age: Int, attachment: Int): HasImageView
val previousFrameCount: Int How many completed frames are readable while one is being written: frameCount less the slot in use, and the length of previousFrameImageViews — the array size a sampler2D history binding declares.
override fun blitToScreen(renderer: Renderer, target: RenderTarget, attachment: Int, name: String, viewport: Rect?, sourceRect: Rect?, blend: Boolean, additive: Boolean, sourceLayer: Int, warper: String?, numSegmentsW: Int, numSegmentsH: Int, configure: ColorGrade.() -> Unit): ModulePointer Blits the latest ping-pong output to a screen target.
override fun debugBlitToScreen(renderer: Renderer, target: RenderTarget, attachment: Int, name: String, viewport: Rect?, sourceRect: Rect?, blend: Boolean, additive: Boolean, sourceLayer: Int, warper: String?, numSegmentsW: Int, numSegmentsH: Int, configure: ColorGrade.() -> Unit): ModulePointer
override fun beforeRender(index: Int)
override fun afterRender(index: Int)

FrameCadencedPingPongRenderToTexture

class FrameCadencedPingPongRenderToTexture(renderer: Renderer = fromContext(), framesPerPingPong: Int = 1, frameCount: Int = 2, parameters: RTTParameters.() -> Unit = {}) : PingPongRenderToTexture(renderer, frameCount, parameters)

A PingPongRenderToTexture that only advances the frame ring once every framesPerPingPong render calls. Between advances, thisImageView remains the latest completed texture and otherImageView remains the previous one, so downstream modules can crossfade from other to this with crossfade.

Declaration Summary
var framesPerPingPong: Int How many render calls one ring slot is held for: 1 advances every frame, 4 renders once and shows that result for four. Writing a value below 1 stores 1, and shortening the period also pulls framesSincePingPong back inside it, so the next advance is not stranded.
var framesSincePingPong: Int Render calls since the last advance, counting 0 on the frame that advanced. It drives crossfade and framePhase, and reaching framesPerPingPong less one is what makes the next call render.
var pingPongCount: Long How many times the ring has advanced, which is how many frames this target has actually rendered as against the render calls it was given. Zero until the first, which is why the first call always renders whatever the cadence says.
val framesUntilPingPong: Int Render calls still to be held before the ring advances again: 0 means the next call renders. It is also 0 before the first advance, which is not waiting on the cadence.
val crossfade: Float 0 immediately after a ping-pong render, 1 on the last held frame before the next one. Intended for mix(other, this, crossfade).
val framePhase: Float Similar to crossfade, but normalized by the whole cadence period and therefore stays below 1 before the next ping-pong.
override val frameImageViewThisIndex: Int
override val thisImageView: HasImageView
override val otherImageView: HasImageView
override fun thisImageViewFor(attachment: Int): HasImageView
override fun otherImageViewFor(attachment: Int): HasImageView
override fun render(command: CommandBuffer, index: Int)
override fun toString(): String

PointAccumulation.kt

Accumulating points: GPU points plotted into a float image that fades a little every frame, so they build up into dust, trails or a density picture. PointAccumulation is the target; AccumulatedPoint records are what it plots.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/PointAccumulation.kt — read the source

PointBlend

enum class PointBlend

How points combine with what is already in a PointAccumulation.

Constants: ADDITIVE, ALPHA.

PointToneMap

enum class PointToneMap

How a PointAccumulation's accumulated light becomes a colour between 0 and 1 on screen.

Constants: LINEAR, EXPONENTIAL, REINHARD, LOG.

PointAccumulationSettings

open class PointAccumulationSettings

Settings for a PointAccumulation. Everything except the size and format can change while it runs: the passes read these each frame.

Declaration Summary
var name: String Names the modules this accumulation installs, so a second one with the same name replaces it.
var width: Int The target's size in pixels; 0 means the renderer's window.
var height: Int The other half of that size; 0 means the renderer's window. Fixed once the accumulation runs.
var format: Int The float format of the accumulation. 16-bit float is enough while fade is above zero.
var fade: Double How much of the picture disappears each frame, 0..1. 0 keeps everything ever plotted.
var blend: PointBlend How a point combines with what is already there — PointBlend.ADDITIVE light, or PointBlend.ALPHA paint.
var toneMap: PointToneMap How the accumulated light is brought back into 0 to 1 for display; see PointToneMap.
var exposure: Double Multiplies the accumulated light before the tone map.
var whitePoint: Double Where PointToneMap.REINHARD and PointToneMap.LOG reach full brightness.
var intensity: Double Multiplies every point's alpha as it is plotted, so a denser system can keep the same look.
var background: Vec4 The ground the light is shown on, as an sRGB colour (what a CSS colour or hexColor("#010301") gives).
var viewport: Rect The world rectangle that fills the target, as Rect(xMin, yMin, width, height) with y up.
var keepAspect: Boolean Grow viewport to the target's shape, so a circle stays round, instead of stretching it.
fun viewport(xMin: Double, xMax: Double, yMin: Double, yMax: Double) The viewport by its edges, the way a canvas demo writes it.

PointAccumulation

class PointAccumulation(val renderer: Renderer, settings: PointAccumulationSettings) : Target, HasImageView

Points that accumulate: a floating-point PingPongRenderToTexture that every frame keeps 1 - fade of what it held, then lets point modules draw on top with PointBlend.ADDITIVE or PointBlend.ALPHA blending, and shows the result tone mapped over a background colour.

Declaration Summary
val renderer: Renderer The renderer to build on.
var settings: PointAccumulationSettings The settings, and the handle for changing them live. Every pass reads them each frame, so swapping in a new object (as a re-run does) takes effect on the next frame; the size and PointAccumulationSettings.format stay as built.
val width: Int Width of the accumulation in pixels.
val height: Int Height of the accumulation in pixels.
val target The accumulation itself. Point modules are added to it; latestImageView is what to sample.
var advance: () -> Boolean Called once per frame: return false to hold the picture where it is this frame.
val fadeModule: ModulePointer The pass that carries the previous frame forward, faded. Always runs first.
override val imageView: ImageView The latest accumulated frame, so the accumulation can be sampled like any texture.
val latestImageView: HasImageView The latest accumulated frame.
override fun render(command: CommandBuffer, index: Int) Runs the fade and every point module, unless advance says to hold.
fun clear() Erase the picture on the next frame that advances.
fun effectiveViewport(): Vec4 The viewport actually used, (xMin, yMin, xMax, yMax): PointAccumulationSettings.viewport, grown about its centre to the target's shape when PointAccumulationSettings.keepAspect is set.
fun pointParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters Parameters for a module that draws into this accumulation: the target's format, no depth, and the blend from PointAccumulationSettings.blend, expecting premultiplied colour. Hand them to moduleHere(...), then add the module with add.
fun add(name: String, module: ModulePointer) Add a module that draws into this accumulation, after the fade.
fun plot(points: () -> HasBuffer, count: Int, name: String = "${settings.name}-plot"): ModulePointer Plot count AccumulatedPoint records from points every frame, mapped from world coordinates through the viewport. points is read when each frame is drawn, so it may hand back a different buffer each time. A plot under a name already in use replaces it. Returns the module, already added.
fun plot(points: HasBuffer, count: Int, name: String = "${settings.name}-plot"): ModulePointer plot for a buffer that does not change.
fun installInto(renderTarget: RenderTarget = renderer.renderList, withDisplay: Boolean = true, viewport: Rect? = null): ModulePointer? Run this accumulation before renderTarget's own modules each frame and, unless withDisplay is false, show it there with display. Installing again under the same name replaces the earlier one. Returns the display module, or null.
fun display(renderTarget: RenderTarget = renderer.renderList, name: String = "${settings.name}-display", viewport: Rect? = null): ModulePointer Draw the accumulation to renderTarget, tone mapped and over the background. viewport is where on the target it lands; null fills it.

PointShaders

object PointShaders

The packaged GLSL behind PointAccumulation and afield.simulation.particles.IteratedSystem. #include "points/system" in a compute shader reaches system.glsl, the iterated-system harness.

Declaration Summary
fun has(name: String): Boolean Whether points/<name> names one of these sources, with or without .glsl.
fun source(name: String): String The source of name (system, system.glsl, ...).

RenderTarget.kt

The render pass that draws a window's swapchain image: an ordered collection of modules, mesh modules and other targets recorded into one frame.

Add passes with += on a RenderTarget; a RendersAfter pass is moved behind the target it depends on before the list is recorded.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/RenderTarget.kt — read the source

RenderTarget

class RenderTarget(val renderer: Renderer, private val contextProvider: () -> Renderer.WindowContext = { renderer.windowContext(0) }, val windowIndex: Int = 0) : Target

Records a window's frame from its named modules, mesh modules and other Targets, in insertion order.

Declaration Summary
val renderer: Renderer The renderer that owns the window this target draws, and that submits the command buffer the passes record into.
val windowIndex: Int Which of the renderer's windows this target draws, matching the context it reads its swapchain and depth attachments from. renderer.renderTargetFor(windowIndex) is how a box reaches the one it wants, and the main window's is Renderer.renderList.
var modules The modules drawn into this window, by name, recorded in insertion order before meshModules. Add with +=; passing a name-to-ModulePointer pair takes any entry already under that name out first, so re-adding one moves it to the end of the order.
var meshModules The mesh-shader modules drawn into this window, by name, recorded after modules and in their own insertion order.
var others Targets rendered after this one's pass has ended, by name — the render-to-textures and compute targets chained behind the window. A RendersAfter entry is moved behind the target it names before the list runs.
var preRecordedInsideRendering: List<CommandBuffer>? Secondary command buffers to execute inside the rendering instance in place of recording modules and meshModules this frame. It is consumed: the field is set back to null once the frame has used it.
var preTargets Targets rendered before any of this one's render-pass work, by name — where an upload or streaming copy goes, since a transfer cannot be recorded inside dynamic rendering.
operator fun plusAssign(m : Pair<String, MeshModulePointer>)+6 overloads · also Pair<String, ModulePointer>, ModulePointer, Pair<String, Module>, MeshModulePointer, Pair<String, MeshModule>, Pair<String, Target>
var seenBefore The swapchain image indices this target has already presented, which decide whether a frame's first barrier declares the old layout as present-source or undefined. It is cleared when the swapchain is recreated, since a resize hands back brand-new images.
override fun render(command: CommandBuffer, index: Int)

RendersAfter

interface RendersAfter

A pass that has to render after another Target in the same list: a publisher after the texture it publishes. Wherever it was added, RenderTarget moves it behind rendersAfter before it iterates.

Declaration Summary
val rendersAfter: Target The target this pass has to follow. RenderTarget looks for it in the same others map and moves this entry behind it; a target that is not in that map leaves the order alone.

RenderToTexture.kt

Offscreen rendering: RenderToTexture draws modules into its own color (and optional depth) images, which other modules then sample as textures.

A box beneath the renderer builds one with a configuration block over RTTParameters (size, formats, attachment count, mip levels, clearing, depth), registers modules to draw into it, and hands its image views to whatever samples the result. The same target can be blitted to the window, read back to the CPU, or saved as a JPEG. PingPongRenderToTexture and MultiPingPongRenderToTexture build feedback and multi-pass chains from it.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/RenderToTexture.kt — read the source

RTTParameters

data class RTTParameters(var width: Int = 1024, var height: Int = 1024, var mipLevels: Int = 1, var format: Int = VK_FORMAT_B8G8R8A8_SRGB, var colorAttachments: Int = 1, var colorFormats: List<Int> = emptyList(), var colorLayerCount: Int = 1, var forceColorArrayView: Boolean = false, var viewports: List<afield.app.protocol.Rect> = emptyList(), var multiviewViewMask: Int = 0, var externalColorViews: Map<Int, HasImageView> = emptyMap(), var clearColor: Vec4 = vec(0, 0, 0, 1), var clearFirst: Boolean = true, var colorStoreOp: Int = VK_ATTACHMENT_STORE_OP_STORE, var colorSampled: Boolean = true, var colorTransferSrc: Boolean = true, var colorTransferDst: Boolean = false, var colorStorage: Boolean = false, var generateColorMipmaps: Boolean = true, var mipBlitFilter: Int = VK_FILTER_LINEAR, var useDepth: Boolean = true, var depthFormat: Int = Options.defaultDepthFormat, var clearDepth: Float = 1f, var clearStencil: Int = 0, var depthLoadOp: Int = VK_ATTACHMENT_LOAD_OP_CLEAR, var depthStoreOp: Int = VK_ATTACHMENT_STORE_OP_STORE, var depthSampled: Boolean = false, var debugName: String = "", var exportMetalIOSurface: Boolean = false, var importMetalIOSurface: Long = 0, var exportOpaqueFd: Boolean = false, var exportDmaBuf: Boolean = false)

Configuration for a RenderToTexture: size, formats, attachments, clearing and depth, set in its constructor block.

Declaration Summary
var width: Int Width of every attachment, in pixels.
var height: Int Height of every attachment, in pixels.
var mipLevels: Int Mip levels on the color attachments; more than one asks for generateColorMipmaps.
var format: Int The VK_FORMAT_* used for any color attachment colorFormats does not name.
var colorAttachments: Int How many color attachments to create.
var colorFormats: List<Int> Per-attachment formats; empty uses format throughout, and a list must be colorAttachments long.
var colorLayerCount: Int Single layer count for all color attachments; 0 infers it from the external views.
var forceColorArrayView: Boolean Expose color attachments as sampler2DArray even for a single layer.
var viewports: List<afield.app.protocol.Rect> Optional viewport array override.
var multiviewViewMask: Int Non-zero enables VK_KHR_multiview / dynamic-rendering multiview.
var externalColorViews: Map<Int, HasImageView> Optional externally-owned attachments, by slot.
var clearColor: Vec4 The RGBA the color attachments are cleared to when clearFirst.
var clearFirst: Boolean Clear the color attachments at the start of the pass; false loads the previous contents instead.
var colorStoreOp: Int The color attachments' VK_ATTACHMENT_STORE_OP_*.
var colorSampled: Boolean Whether the result will be sampled, which controls usage flags and the final transition.
var colorTransferSrc: Boolean Allow the produced color image to be used as a transfer source — mirroring, or copying into an OpenXR swapchain.
var colorTransferDst: Boolean Allow the produced color image to be used as a transfer destination.
var colorStorage: Boolean Opt in when an external compute pass writes the color attachment as a storage image.
var generateColorMipmaps: Boolean Generate the color mip chain automatically, when mipLevels is more than one.
var mipBlitFilter: Int Filter for the mip-generating blits; downgraded to VK_FILTER_NEAREST where linear is unsupported.
var useDepth: Boolean Create a depth (by default depth+stencil) attachment and depth-test against it.
var depthFormat: Int The depth attachment's VK_FORMAT_*; depth+stencil by default.
var clearDepth: Float The depth value cleared to when depthLoadOp clears.
var clearStencil: Int The stencil value cleared to when depthLoadOp clears.
var depthLoadOp: Int The depth attachment's VK_ATTACHMENT_LOAD_OP_*.
var depthStoreOp: Int The depth attachment's VK_ATTACHMENT_STORE_OP_*.
var depthSampled: Boolean Set true only if you intend to sample the depth later.
var debugName: String Name for this target in Vulkan debug labels; empty synthesizes one from a counter and the allocation site.
var exportMetalIOSurface: Boolean Create color attachments with IOSurface-exportable backing (MoltenVK) so the target can be handed zero-copy to a hardware video encoder (field-encode). Opt-in; macOS/MoltenVK only.
var importMetalIOSurface: Long Back color attachment 0 with an externally-created IOSurface, given as its IOSurfaceRef pointer, instead of exporting one. Preferred for streaming: an IOSurfaceCreate-born surface carries a real pixelFormat that CoreVideo/VideoToolbox can wrap, where MoltenVK's export leaves it 0. macOS/MoltenVK only; the surface's format and size must match this target's.
var exportOpaqueFd: Boolean Create color attachments with opaque-fd-exportable backing (VK_KHR_external_memory_fd) so the target can be handed zero-copy to NVENC through CUDA external memory import (field-encode-linux). Opt-in; Linux/NVIDIA only; see VulkanExternalMemory.
var exportDmaBuf: Boolean Create color attachments with dma-buf-exportable backing and a driver-chosen DRM format modifier, so the target can be handed to any dma-buf importer — including Field's own external-browser receiver, which is what makes that receiver testable without a browser. Opt-in; Linux only; see LinuxDmaBufExport.

RenderToTexture

open class RenderToTexture(val renderer: Renderer = fromContext(), parameters: RTTParameters.() -> Unit = {}) : Target, HasImageView, HasImageViewArray, HasDefaultTextureSampler, PreparesSampledImages

An offscreen target holding one set of color attachments, any number of them, which its modules draw into and other modules sample as textures. Optionally a depth+stencil attachment as well, to depth-test against while rendering. The attachments are single-buffered, so reading last frame's output while writing this one is PingPongRenderToTexture's job rather than this class's.

Declaration Summary
val renderer: Renderer The renderer whose device allocates the attachments and whose swapchain depth sets numImages.
var parameters The RTTParameters this target was built with. The images were created from them at construction, so changing a size or format here does not reallocate; the per-frame fields (RTTParameters.clearFirst, RTTParameters.clearColor, RTTParameters.colorStoreOp, RTTParameters.depthLoadOp, RTTParameters.viewports) are re-read each time the pass records.
override var defaultSampler: TextureSampler?
fun setDefaultSampler(anisotropyEnable: Boolean = true, filter: Int = VK_FILTER_LINEAR, mipmapMode: Int = if (filter == VK_FILTER_LINEAR) VK_SAMPLER_MIPMAP_MODE_LINEAR else VK_SAMPLER_MIPMAP_MODE_NEAREST, addressModeU: Int = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, addressModeV: Int = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, addressModeW: Int = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE): TextureSampler
fun repeatU(anisotropyEnable: Boolean = true, filter: Int = VK_FILTER_LINEAR, mipmapMode: Int = if (filter == VK_FILTER_LINEAR) VK_SAMPLER_MIPMAP_MODE_LINEAR else VK_SAMPLER_MIPMAP_MODE_NEAREST): TextureSampler
override val imageView: ImageView
val colorImageRefs: MutableList<Image?> The Image behind each color attachment, in slot order. An entry is null where the slot came from RTTParameters.externalColorViews and that view has no Image of its own, so read colorImageHandles when all you need is the Vulkan handle.
val colorImageHandles: MutableList<Long> The VkImage handle of each color attachment, external slots included; its size is the number of attachments, which every module registered here has to declare.
override val imageViewArray: List<HasImageView>
val colorFormats: List<Int> The resolved VK_FORMAT_* of each color attachment: RTTParameters.colorFormats when it was given, otherwise RTTParameters.format repeated. A module's declared formats have to match this list exactly.
val colorLayerCount: Int Array layers in each color attachment. RTTParameters.colorLayerCount when positive; 0 takes the count from the external views, which all have to agree.
var depthImageView: ImageView? The depth(+stencil) attachment's view, or null when RTTParameters.useDepth is off. Its aspect mask covers stencil too for a combined format, which is why sampling uses depthSampledImageView.
var depthSampledImageView: ImageView? A depth-aspect-only view of the same image, for sampling the depth result. It is a second view only when the format carries stencil and RTTParameters.depthSampled is set; otherwise it is depthImageView itself, and null when there is no depth attachment.
var depthImage: Image? The depth(+stencil) image, or null when RTTParameters.useDepth is off. It carries colorLayerCount layers so a multiview or layered pass can depth-test.
val width: Int The attachment width the images were created at, read back from parameters.
val height: Int The attachment height the images were created at, read back from parameters.
val mipLevels: Int Mip levels on the color attachments. Above one, the sampled views cover the whole chain once the mips have been generated, and only mip 0 before that.
var modules The modules drawn into this target, by name, recorded in insertion order. Add with rtt += "name" to module, which checks the module's attachment count, formats, view mask and viewport count against this target first; a module written for the window fails that check until +target appears in its own parameters. Adding a module under a name already here replaces it without moving it in the order.
var meshModules Procedural geometry, drawn after conventional modules in this same rendering instance.
fun saveJpgAsync(outputPath: String, attachment: Int = 0, quality: Int = 90): CompletableFuture<String> Queue a non-blocking save of a color attachment to JPEG.
fun saveJpegAsync(outputPath: String, attachment: Int = 0, quality: Int = 90): CompletableFuture<String>
fun readPixelsAsync(attachment: Int = 0): CompletableFuture<ByteArray> Queue a copy of a color attachment's pixels exactly as the attachment stores them — for a float target, the float values the shader wrote, unclamped, NaN included — which a JPEG or an 8-bit preview cannot carry.
fun launchJpgSave(outputPath: String, attachment: Int = 0, quality: Int = 90): CompletableFuture<String>
fun recordColorTransferSource(command: CommandBuffer, attachment: Int = 0, copy: (vkImage: Long) -> Unit) Record copy with color attachment in TRANSFER_SRC_OPTIMAL, then restore the layout it had — the same bracket the file saves below use, for a consumer outside this class that reads the attachment on the same command buffer. FieldRTTStreamer uses it to copy each rendered frame into its IOSurface on KosmicKrisp. Outside a render pass.
fun recordColorTransferDestination(command: CommandBuffer, attachment: Int = 0, copy: (vkImage: Long) -> Unit) GPU producer upload into a sampled attachment. Outside a render pass; no host staging.
override fun prepareSampledImagesFor(command: CommandBuffer)
var commandBuffers: Array<CommandBuffer> One primary command buffer per swapchain image, allocated at construction. The pass itself records into the command buffer handed to render, so these are for code that submits a target on its own; nothing in this class submits them, and a caller that allocated a target outside the renderer's frame loop frees them along with the target.
var fences: Array<afield.graphics.vulkan.device.Fence> One fence per swapchain image, created already signalled, to pair with commandBuffers in the same off-frame submission.
val numImages How many command buffers and fences this target holds: the renderer's swapchain image count, read once at construction. The attachments themselves are single-buffered; PingPongRenderToTexture is the variant that rings them.
data class ColorGrade(var exposure: Vec4 = vec(1f, 1f, 1f, 1f), var gamma: Vec4 = vec(1f, 1f, 1f, 1f), var offset: Vec4 = vec(0f, 0f, 0f, 0f), var sampleWeight: Float = 1f)
open fun blitToScreen(renderer: Renderer = this.renderer, target: RenderTarget = renderer.renderList, attachment: Int = 0, name: String = "rtt-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: ColorGrade.() -> Unit = {}): ModulePointer Simple on-screen blit of a color attachment from this RTT. Generates a fullscreen triangle mesh internally (no vertex inputs) and wires a small grading push-constant block.
open fun debugBlitToScreen(renderer: Renderer = this.renderer, target: RenderTarget = renderer.renderList, attachment: Int = 0, name: String = "rtt-debug-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: ColorGrade.() -> Unit = {}): ModulePointer
fun debugBlitToScreen(windowIndex: Int, renderer: Renderer = this.renderer, attachment: Int = 0, name: String = "rtt-debug-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: ColorGrade.() -> Unit = {}): ModulePointer
fun debugBlitToScreen(window: Window, renderer: Renderer = this.renderer, attachment: Int = 0, name: String = "rtt-debug-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: ColorGrade.() -> Unit = {}): ModulePointer
fun blitToScreen(windowIndex: Int, renderer: Renderer = this.renderer, attachment: Int = 0, name: String = "rtt-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: ColorGrade.() -> Unit = {}): ModulePointer Blits a color attachment to the renderer window identified by windowIndex.
fun blitToScreen(window: Window, renderer: Renderer = this.renderer, attachment: Int = 0, name: String = "rtt-blit", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false, sourceLayer: Int = 0, warper: String? = null, numSegmentsW: Int = 1, numSegmentsH: Int = 1, configure: ColorGrade.() -> Unit = {}): ModulePointer Blits a color attachment to the screen for window.
override fun render(command: CommandBuffer, index: Int)
operator fun plusAssign(m: Pair<String, MeshModulePointer>)+2 overloads · also Pair<String, ModulePointer>, Pair<String, Module>
override fun toString(): String

RollingCapture.kt

Reads a rendered image back to the CPU a strip at a time, so a full-frame capture never stalls a frame.

RollingCapture copies one horizontal strip per frame into a snapshot image, then copies the finished snapshot into a CPU-visible staging buffer.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/RollingCapture.kt — read the source

RollingCapture

class RollingCapture(private val renderer: Renderer, private val source: Image, private val rowsPerFrame: Int = 16, private val sourceLayout: Int = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, private val downsample: Int = 1)

Rolling capture that copies a horizontal strip from a source image into an internal snapshot image each frame. Once a full frame has been copied, you can copy the snapshot to a CPU-visible buffer (e.g., on a low-priority transfer queue) without stalling rendering.

Declaration Summary
val snapshotView A view of the snapshot image itself, so a pass can sample the partly-rebuilt frame on the GPU without waiting for a CPU readback. Expect to see the strip boundary: the rows above recordStrip's next row are from this sweep and the rows below are from the previous one.
val stagingBuffer Host-visible, host-coherent buffer holding one whole captured frame, tightly packed at the capture's own size after downsample. copySnapshotToStaging fills it; nothing here synchronises the read, so wait on that copy's fence before looking at the bytes.
var fullFrameReady: Boolean True once the strips have covered the whole image and the snapshot is a complete frame. While it is true recordStrip does nothing, so capture pauses rather than overwriting a frame nobody has taken; copySnapshotToStaging clears it and the next strip starts a new sweep.
fun recordStrip(command: CommandBuffer) Record a strip copy from the source image into the snapshot image. Call this at the end of your frame on the graphics command buffer.
fun copySnapshotToStaging(command: CommandBuffer) Copy the accumulated snapshot image to the staging buffer. Call on a transfer/graphics command buffer after checking fullFrameReady.
fun copySnapshotToStagingAsync(): Fence Convenience: records and submits the staging copy on the internal transfer queue. Returns a fence you can wait on from a CPU thread.
fun startBackgroundRead(onFrame: (ByteBuffer) -> Unit, sleepMs: Long = 1L) Start a background thread that waits for full captures, kicks the async staging copy, waits the fence, and hands the CPU buffer to onFrame.
fun stopBackgroundRead()
fun cleanup()
fun install(r : Renderer, onFrame: (ByteBuffer) -> Unit, name : String = "__rolling_capture__")

SideBySideStereoTarget.kt

Stereo rendering into one packed image: SideBySideStereoTarget draws the scene once per eye and lays the two views side by side, left eye on the left half.

With an OpenXRHmdTarget attached, the per-eye view and projection matrices come from the headset and the packed image can feed its swapchains; without one, an internal stereo camera supplies them, so the same box code renders a stereo pair on a desktop window.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/SideBySideStereoTarget.kt — read the source

Used in: Field on Apple Vision Pro

SideBySideStereoTarget

class SideBySideStereoTarget(val renderer: Renderer = fromContext(), val eyeWidth: Int = renderer.swapChain.swapChainExtent.width() / 2, val eyeHeight: Int = renderer.swapChain.swapChainExtent.height(), var openXR: OpenXRHmdTarget? = null) : afield.graphics.vulkan.Target, HasImageView

Side-by-side stereo helper that renders two eye passes into a single RTT (left half / right half).

Declaration Summary
val renderer: Renderer The renderer whose device owns the eye passes and the packed image. Found in the box graph by default.
val eyeWidth: Int Width of one eye's pass in pixels. Defaults to half the swapchain's, so the pair fills the window.
val eyeHeight: Int Height of one eye's pass in pixels. Defaults to the swapchain's full height.
var openXR: OpenXRHmdTarget? The headset to take per-eye view and projection matrices from, and to hand packed to. Null renders the same stereo pair from stereoCamera instead, which is the desktop-window path.
val stereoCamera The camera the eye passes use when neither poseSource nor openXR supplies a head.
val packed The finished pair, 2 * eyeWidth by eyeHeight, left eye in the left half. This is what a desktop window samples and what OpenXRHmdTarget.copySideBySideToEyes copies halves out of; it carries both transfer usages so either path works.
override val imageView The packed pair, so a SideBySideStereoTarget is itself usable anywhere a HasImageView is — a desktop window, a postprocess pass, an OpenXR side-by-side source.
var currentEyeIndex: Int Which eye the modules are being drawn for right now: 0 left, 1 right, -1 between the two passes. Module code reads this to pick per-eye data, and it is the eye currentEyeViewMatrix and currentEyeProjectionMatrix answer for.
var poseSource: StereoPoseSource? A remote head to render from, in preference to openXR and to stereoCamera.
var renderedPoseFrameId: Long Identity of the pose the most recent frame was rendered from, or -1 before either the remote source or OpenXR has located a pose.
var renderedPoseDisplayTime: Double Predicted display time for renderedPoseFrameId, in seconds in the selected source's clock.
fun registerModule(name: String, factory: () -> ModulePointer) Register a module factory; it will be instantiated once per eye.
operator fun plusAssign(entry: Pair<String, () -> ModulePointer>)
fun attachToOpenXR(target: OpenXRHmdTarget) Convenience: attach this packed RTT as the OpenXR side-by-side source.
fun currentEyeViewMatrix(out: Matrix4f = Matrix4f()): Matrix4f Current eye view matrix (a latched poseSource first, then OpenXR, then stereoCamera).
fun currentEyeProjectionMatrix(out: Matrix4f = Matrix4f()): Matrix4f Current eye projection matrix (a latched poseSource first, then OpenXR, then stereoCamera).
fun controllerState(hand: Hand = Hand.RIGHT, out: ControllerState = ControllerState()): ControllerState Controller state passthrough to OpenXR when attached; otherwise returns a simple mock right-hand controller positioned via updateMockControllerFromRay or updateMockControllerFromCamera.
fun updateMockControllerFromCamera(distanceMeters: Float = 1f) Position the mock controller 1m along the current camera forward vector.
fun updateMockControllerFromRay(origin: Vector3f, direction: Vector3f, distanceMeters: Float = 1f) Position the mock controller along an arbitrary ray (origin + direction * distance).
fun enableMouseController(mouse: MouseEvents = renderer.window.mouseEvents ?: MouseEvents(renderer), name: String = "sbs-mouse-controller", distanceMeters: Float = 1f): () -> Unit Attach mouse handlers to drive the mock controller. Re-registering replaces previous handlers.
override fun render(command: CommandBuffer, index: Int)

StereoCamera

class StereoCamera(val base: Camera = Camera())

Minimal stereo camera: offsets along the right vector by +/-IPD/2, converges on a focus point, and supports asymmetric frustum offsets through the base projection.

Declaration Summary
var ipd: Float Interpupillary distance in world units, split half to each side of base along its right vector. The default 0.064 is the usual adult average in metres.
var convergenceDistance: Float Distance multiplier for the stereo convergence point, measured from the base camera toward its target along the forward vector.
var asymmetricFrustumOffset: Double Shifts each eye's principal point instead of toeing its view in: the left eye gets -asymmetricFrustumOffset and the right +asymmetricFrustumOffset as the base projection's ox, so the units are half-widths of what that projection frames at its match depth. Zero leaves both frusta centred, and convergence is then convergenceDistance's business alone.
data class EyePose(val eye: Camera)
fun eyes(): Pair<EyePose, EyePose>
fun eyeViewMatrices(): List<Matrix4f>
fun projectionMatrices(): List<Matrix4f>

StereoEyeCopy.kt

src/jvmMain/kotlin/afield/graphics/vulkan/targets/StereoEyeCopy.kt — read the source

StereoPoseSource.kt

The interface for a stereo head pose that arrives from somewhere other than OpenXR, such as a remote headset.

A StereoPoseSource is latched once per frame so both eyes render from the same pose, and it reports which frame that pose belongs to.

src/jvmMain/kotlin/afield/graphics/vulkan/targets/StereoPoseSource.kt — read the source

StereoPoseSource

interface StereoPoseSource

A source of stereo viewing geometry that is neither OpenXR nor a desktop mock — a head somewhere else, reporting where it is.

Declaration Summary
fun latch(): Boolean Take the newest usable pose as the one to render this frame from.
val latchedFrameId: Long Identity of the latched pose, or -1 when nothing has ever been latched.
val latchedDisplayTime: Double The head's own predicted display time for the latched pose, in the head's clock (seconds). Meaningless against this machine's clock; carried so a consumer can hand it back to the head.
val eyeCount: Int Views the latched pose carried. One on the visionOS simulator, two on hardware.
fun eyeViewMatrix(eyeIndex: Int, out: Matrix4f = Matrix4f()): Matrix4f World→eye for the latched pose. Indices past eyeCount clamp to the last view.
fun eyeProjectionMatrix(eyeIndex: Int, out: Matrix4f = Matrix4f()): Matrix4f Projection for the latched pose, in this renderer's own depth convention.
fun headPose(outPosition: Vec3 = Vec3(), outOrientation: Quat = Quat()): Boolean Head pose of the latched frame. False when nothing is latched.
fun controller(hand: OpenXRHmdTarget.Hand, out: OpenXRHmdTarget.ControllerState): Boolean Fill out with hand's controller as of the latched pose; false when this source has none.

16bits.kt

Half-precision (binary16) conversion and packing for data headed to GPU buffers.

floatToHalfBits and halfBitsToFloat convert single values, packHalf1x16 and packHalf2x16 pack them as GLSL's packHalf2x16 does, and the .half views (Vec2h, Vec3h, Vec4h) mark a Field vector for 16-bit storage.

src/jvmMain/kotlin/afield/graphics/vulkan/util/16bits.kt — read the source

Top-level functions

Declaration Summary
fun packHalf1x16(x: Float): Int Converts one float to binary16 and returns the sixteen bits zero-extended into an Int.
fun packHalf2x16(x: Float, y: Float): Int Packs two floats into one Int as binary16 pairs, with x in the low sixteen bits and y in the high sixteen: the layout GLSL's packHalf2x16 produces and unpackHalf2x16 reads.
fun floatToHalfBits(value: Float): Short Convert a Float to IEEE 754 binary16 bit pattern (half float).
fun halfBitsToFloat(bits: Short): Float Convert an IEEE 754 binary16 bit pattern (half) to Float.

Top-level values

Declaration Summary
val Vector2f.half: Vec2h Marks this vector for 16-bit storage: Vector2f(0.4f, 0.9f).half is the idiom for giving a push-constant member or a buffer field its half-precision form.
val Vector3f.half: Vec3h Marks this vector for 16-bit storage, wrapping it rather than copying it.
val Vector4f.half: Vec4h Marks this vector for 16-bit storage, wrapping it rather than copying it.

Vec2h

data class Vec2h(val v: Vector2f)

A two-component vector marked for 16-bit storage, holding the full-precision Vector2f until something writes it out.

Vec3h

data class Vec3h(val v: Vector3f)

Three components marked for 16-bit storage. The packed form is two uints — x and y in the first, z in the low half of the second — so it costs the same eight bytes a uvec2 does.

Vec4h

data class Vec4h(val v: Vector4f)

Four components marked for 16-bit storage, packed into two uints and read back in the shader as a uvec2: eight bytes where a vec4 takes sixteen, at enough precision for a colour.

AutoDisposer.kt

Releases Vulkan handles when the Kotlin object that owned them is garbage collected.

A resource calls AutoDisposer.register with a cleanup lambda and keeps the returned AutoDisposer.Token. When the object has been collected, the renderer's frame loop calls AutoDisposer.drain and queues the cleanup on its retirement queue. Explicit cleanup calls AutoDisposer.Token.disable first, so a resource is released at most once whichever path gets there first. Tests are in AutoDisposerTest.

src/jvmMain/kotlin/afield/graphics/vulkan/util/AutoDisposer.kt — read the source

AutoDisposer

object AutoDisposer

GC-driven disposal, with optional device ownership for deterministic shutdown. Cleanup actions must capture handles, not the registered object itself.

Declaration Summary
class Token internal constructor(internal val ref: PhantomReference<Any>, internal val name: String, internal val site: StackTraceElement?, internal val owner: Any?, internal val order: Long)
fun register(target: Any, name: String = target.javaClass.simpleName.ifEmpty { "unnamed" }, owner: Any? = null, cleanup: () -> Unit): Token Track target; cleanup runs once after it is collected, or when owner shuts down.
fun drain(owner: Any? = null, enqueue: (Runnable) -> Unit) Poll collected objects, keeping other owners' releases for their own render thread.

Camera.kt

The Vulkan renderer's look-at camera: a position, a target and an up vector, with a SuperProjection for its projection.

Camera supplies the model-view matrix as a uniform and moves in world or camera-local terms: dolly, pan, yaw, pitch, roll, and rotation around either the camera position or the target. OrbitControls drives it from the mouse, and CameraPose.kt converts it to and from a rigid pose.

src/jvmMain/kotlin/afield/graphics/vulkan/util/Camera.kt — read the source

Camera

class Camera(val renderer: Renderer = fromContext())

The Vulkan renderer's look-at camera: a position, a target and an up vector, with a SuperProjection for its projection.

Declaration Summary
var at Where the camera is, in world space. Every movement helper writes through this vector rather than replacing it, so a reference taken once keeps following the camera.
var up Which way is up, in world space. It need not be perpendicular to the view direction: forward and right cope with an oblique one, and reorthogonalizeUp squares it up again — which the rotation helpers do by default, and roll does not.
var target The point the camera looks at. distanceToTarget measures to it, rotateAroundTarget orbits about it, and the default projection takes its focus distance from it.
val forward: Vector3f Forward direction (normalized) from camera position toward target. Falls back to -Z if at == target.
val right: Vector3f Right direction (normalized), derived from forward x up. If up is degenerate, this will be recomputed in a stable way.
var p The projection, built against the renderer's window and told to read its SuperProjection.matchDepth from distanceToTarget. That is installed as SuperProjection.auto, which also re-sizes the projection to the window on every evaluation, so a one-off setAspect or p.resize(w, h) is overwritten on the next frame: to render into an offscreen target whose aspect differs from the window's, replace SuperProjection.auto rather than calling resize once.
fun setAspect(aspect: Float) Manually set projection aspect ratio (width/height).
var m Model-view matrix uniform wrapper.
fun modelView(): Matrix4f Compute model-view matrix from at/target/up.
fun distanceToTarget(): Float Distance from camera to target.
fun lookAt(eye: Vector3f, center: Vector3f, up: Vector3f = this.up, reorthogonalizeUp: Boolean = true) Set camera pose using a classical look-at description.
fun lookAlong(position: Vector3f, direction: Vector3f, upHint: Vector3f = this.up, reorthogonalizeUp: Boolean = true) Look from position in the given direction. direction does not need to be normalized.
fun moveWorld(delta: Vector3f, moveTarget: Boolean = true) Translate in world space.
fun moveWorld(dx: Double, dy: Double, dz: Double, moveTarget: Boolean = true)
fun moveLocal(forward: Double = 0.0, right: Double = 0.0, upAmount: Double = 0.0, moveTarget: Boolean = true) Move in camera-local space.
fun dolly(amount: Double, moveTarget: Boolean = false) Dolly along the view direction.
fun pan(right: Double, upAmount: Double, moveTarget: Boolean = true) Pan in screen space: right/left and up/down in camera-local space. right > 0 moves camera to the right, upAmount > 0 moves upward.
fun rotateAroundAt(angle: Double, axis: Vector3f, rotateUp: Boolean = true, reorthogonalizeUp: Boolean = true) General rotation of view around the camera position (at).
fun rotateAroundTarget(angle: Double, axis: Vector3f, rotateUp: Boolean = true, reorthogonalizeUp: Boolean = true) General orbit of the camera position around the target.
fun yaw(angle: Double, aroundTarget: Boolean = false, rotateUp: Boolean = true, reorthogonalizeUp: Boolean = true) Yaw around camera up axis.
fun pitch(angle: Double, aroundTarget: Boolean = false, rotateUp: Boolean = true, reorthogonalizeUp: Boolean = true) Pitch around camera right axis.
fun roll(angle: Double, reorthogonalizeUp: Boolean = false) Roll around the view direction, rotating up but leaving at and target fixed.
fun reorthogonalizeUp() Make up orthogonal to the current forward direction, and ensure a stable, non-degenerate up vector.

CameraPose.kt

Rigid camera poses for blending between viewpoints: CameraPose stores a Camera's placement as a dual quaternion plus a focus distance.

pose reads one from a camera and applyPose writes it back. sclerpTo moves a camera toward a target pose by screw interpolation and nlerpTo by normalised linear interpolation.

src/jvmMain/kotlin/afield/graphics/vulkan/util/CameraPose.kt — read the source

Used in: Keyframes: capturing, persisting and replaying live state

Top-level functions

Declaration Summary
fun Camera.pose(): CameraPose Reads this camera's current placement as a CameraPose, turning its eye, target and up vectors into a rotation, a translation and a focus distance.
fun Camera.applyPose(pose: CameraPose) Moves this camera to pose, writing its eye, target and up vectors in place.
fun Camera.sclerpTo(targetPose: CameraPose, t: Float) Moves this camera a fraction t of the way toward targetPose by screw interpolation, reading its current pose and writing the blended one back.
fun Camera.nlerpTo(targetPose: CameraPose, t: Float) Moves this camera a fraction t of the way toward targetPose by normalised linear interpolation.

CameraPose

data class CameraPose(val transform: DualQuaternionf, val focusDistance: Float)

A camera body transform plus the look-at focus distance needed to reconstruct Field's traditional at/target/up camera state.

Declaration Summary
val transform: DualQuaternionf Where the camera is and which way it faces, as one rigid transform — rotation and translation together in a dual quaternion, which is what makes two poses interpolable as a unit rather than axis by axis.
val focusDistance: Float How far down forward the look-at target sits, in world units. A pose carries this because transform alone cannot say it, and Field's camera is steered by a target point rather than by a rotation.
val position: Vector3f The eye point, read out of transform as its translation.
val rotation: Quaternionf The camera's orientation, read out of transform.
val forward: Vector3f The viewing direction: the OpenGL-style -Z axis turned by rotation, and a unit vector.
val up: Vector3f The camera's up axis: +Y turned by rotation. Already perpendicular to forward, since a rotation is applied to both.
val target: Vector3f The point the camera looks at, focusDistance along forward from position. This is what a Camera is actually steered by, so it is the value applyPose writes.
fun nlerp(other: CameraPose, t: Float): CameraPose Blends toward other by normalised linear interpolation of the two dual quaternions, with focusDistance interpolated linearly alongside.
fun sclerp(other: CameraPose, t: Float): CameraPose Blends toward other by screw interpolation, so the camera turns and travels at a constant rate about a single screw axis. focusDistance is interpolated linearly alongside.

DeferredCleanup.kt

Releases a renderer's deferred GPU resources at shutdown, when no later frame will come to age them out.

While a renderer is running, a resource retired by one frame waits in a queue until the frames that might still be reading it have finished. Shutdown removes that guarantee, so Renderer drains the queues by hand: once the present queue, the graph queue and the device are idle, and then again after releasing the resources a document still held, because releasing those enqueues more work of its own.

Each batch is cleared before it is invoked, so a destructor may enqueue dependent work, and a destructor that throws does not strand the resources queued behind it. Failures are collected and handed back for the owner to report once the rest of its teardown is done, which is why a broken release shows up as one exception with the others suppressed on it rather than as a half-finished shutdown.

src/jvmMain/kotlin/afield/graphics/vulkan/util/DeferredCleanup.kt — read the source

FieldProbe.kt

Samples values out of a running shader into a GPU buffer and shows them in a box's output as a histogram and summary statistics.

A shader includes fieldProbe and writes scalar or vector samples into a buffer bound at FIELD_PROBE_DEFAULT_BINDING; ShaderFieldProbe reads that buffer back off the render thread, builds a FieldProbeSnapshot (counts of NaN, infinite and non-zero samples, ranges, summaries) and, with ShaderFieldProbe.display, publishes it to an output channel at most ShaderFieldProbe.maxDisplayHz times a second. ShaderFieldProbe.ordinalRange limits which invocations record, and ShaderFieldProbe.onSnapshot hands each snapshot to box code.

src/jvmMain/kotlin/afield/graphics/vulkan/util/FieldProbe.kt — read the source

Used in: Field Probes: instrumenting shaders

Top-level functions

Declaration Summary
fun FieldProbeSnapshot.scalarBatch(minSamples: Int = 1, clipLower: Double? = null, clipUpper: Double? = null, clipEpsilon: Double = 1.0e-6): FieldProbeScalarBatch? Packages this snapshot's scalar samples for a controller, returning null when there is not enough to act on.

Top-level values

Declaration Summary
const val FIELD_PROBE_BUFFER_NAME The name of the probe's buffer, which matches the shader's declaration to the buffer the host allocates.
const val FIELD_PROBE_INCLUDE_NAME The name a shader includes to get the probe GLSL: #include "fieldProbe".
const val FIELD_PROBE_DEFAULT_SAMPLES How many samples a probe keeps unless told otherwise, 1024, enough for a histogram to have a shape, small enough to read back every frame.
const val FIELD_PROBE_MAX_SAMPLES The ceiling on ShaderFieldProbe.sampleCapacity, 262144. A larger capacity is clamped to it rather than rejected.
const val FIELD_PROBE_DEFAULT_BINDING Default binding for the probe buffer, 63, just above the statistics buffers at 61 and 62.
const val FIELD_PROBE_DEFAULT_SET Default descriptor set for the probe buffer, 0.

FieldProbeSnapshot

data class FieldProbeSnapshot(val dimensions: Int, val invocationCount: Long, val acceptedCount: Long, val finiteCount: Long, val sampleCount: Int, val nanCount: Long, val infCount: Long, val nonZeroCount: Long, val typeMismatchCount: Long, val exactMin: DoubleArray, val exactMax: DoubleArray, val samples: List<Vec4>)

One frame's worth of probe data: how many times the shader called fieldProbe, what it was holding, and a bounded sample of the values themselves.

Declaration Summary
val dimensions: Int How many components each recorded value had: 1 for a scalar, 2, 3 or 4 for a vector, and 0 if nothing was recorded. The first call of a frame fixes this, and later calls of a different width are counted as typeMismatchCount instead.
val invocationCount: Long How many times the shader called fieldProbe while the probe was enabled, before any filtering: the denominator for how much of the work was looked at. Each call's position in this count is its ordinal.
val acceptedCount: Long How many of those calls got past the ordinal range and the width check, and so contributed to everything below.
val finiteCount: Long How many accepted values were finite in every component. Also the index space the reservoir fills, so min(finiteCount, capacity) is sampleCount.
val sampleCount: Int How many values are actually in samples: finiteCount capped at the configured capacity.
val nanCount: Long How many accepted values had a NaN component. These are counted and dropped, so they do not reach the ranges or the samples; anyNaN is the quick test.
val infCount: Long How many accepted values had an infinite component — counted and dropped like the NaNs, and checked after them, so a value that is both is counted only as NaN.
val nonZeroCount: Long How many finite values had at least one non-zero component, counting only the components dimensions says are real. A probe whose output should not be zero and whose count here is zero has found something.
val typeMismatchCount: Long How many in-range calls were rejected for recording a different number of components than the first call of the frame did. Normally zero; non-zero means two probes of different widths are sharing one buffer, and only the first width is being measured.
val exactMin: DoubleArray The true minimum of each component over every accepted finite value, one entry per recorded component. Computed with integer atomics across the whole frame, so these are not limited to the sampled values. When nothing finite was recorded every entry is NaN, and there is one of them.
val exactMax: DoubleArray The true maximum of each component, on the same terms as exactMin.
val samples: List<Vec4> The retained values, always as Vec4 whatever dimensions is, with the unused components zero. Up to the capacity they are every finite value in order; past it the shader replaces a hashed slot, which keeps the retained set spread across the frame rather than stopping at the first thousand. Read them through scalarValues and the vector forms, which narrow them to the recorded width.
val anyNaN: Boolean Whether any accepted value had a NaN component this frame. The first thing to check when a render goes black.
val anyInf: Boolean Whether any accepted value had an infinite component this frame.
val anyNonZero: Boolean Whether any finite value was not all zeros: the test for "did this code path run and produce anything".
fun scalarValues(): List<Double>? The retained values as plain numbers, or null if this frame recorded vectors rather than scalars. Checking for null is how you find out what the shader was probing.
fun vec2Values(): List<Vec2>? The retained values as two-component vectors, or null if dimensions is not 2.
fun vec3Values(): List<Vec3>? The retained values as three-component vectors, or null if dimensions is not 3.
fun vec4Values(): List<Vec4>? The retained values as four-component vectors, or null if dimensions is not 4. Note that samples is Vec4 whatever the width, while this is null unless the width really was 4.
fun scalarSummary(): SummaryStats? Count, mean, spread and quartiles over the retained scalar values, or null if this frame was not scalar. Computed from the sample rather than from every call, so compare it against exactMin and exactMax rather than treating it as the whole story.
fun vec2Summary(): Vec2SummaryStats? Per-component summary statistics over the retained two-component values, or null if dimensions is not 2.
fun vec3Summary(): Vec3SummaryStats? Per-component summary statistics over the retained three-component values, or null if dimensions is not 3.
fun vec4Summary(): Vec4SummaryStats? Per-component summary statistics over the retained four-component values, or null if dimensions is not 4.
fun percentile(alpha: Double): Double The value at fraction alpha through the sorted scalar samples, interpolating between the two neighbouring samples, with alpha clamped to 0..1.
fun summaryHtml(previous: FieldProbeSnapshot? = null, widthPx: Int = 520, chartHeightPx: Int = 120, decimals: Int = 4, background: String = "#000000", textColor: String = "#0af", accentColor: String = "#0af"): String Renders this snapshot as the HTML card a probe shows in a box's output: the counts, then a histogram and summary for a scalar frame, or a per-component table and a vector-length histogram for a vector one.

FieldProbeScalarBatch

data class FieldProbeScalarBatch(val snapshot: FieldProbeSnapshot, val batch: ScalarSampleBatch)

A probe's scalar samples packaged for a controller, with the snapshot they came from kept alongside.

Declaration Summary
val snapshot: FieldProbeSnapshot The snapshot these samples were taken from, with the counts and exact ranges the batch does not carry.
val batch: ScalarSampleBatch The samples and clip rate, in the form a controller consumes.

ShaderFieldProbe

class ShaderFieldProbe(val box: Box)

A box's shader probe: the buffer a running shader writes samples into, the read-back that brings them off the render thread, and the output card they are shown on.

Declaration Summary
val box: Box The box that owns this probe: whose output channel the card goes to, and whose name a failed read-back is reported against.
var sampleCapacity: Int How many samples are kept and read back each frame, clamped to 1..FIELD_PROBE_MAX_SAMPLES.
var binding: Int The buffer binding the probe's GLSL declares. Change it if the shader's own bindings reach 63.
var descriptorSet: Int The descriptor set the probe's buffer is bound in.
var enabled: Boolean Whether the shader records at all. True by default.
var maxDisplayHz: Double How often the output card may be redrawn, in times a second. Low by default, since building and sending the card is the expensive part of a probe.
var maxReadHz: Double? How often the GPU-to-CPU read may run, for every kind of probe: callback, readLatest or display. Unset, a display-only probe reads at maxDisplayHz and one with a callback or no display channel reads every frame. 0.0 reads every frame.
var ordinalStart: UInt The first invocation ordinal that records, set through ordinalRange or configure.
var ordinalEndExclusive: UInt One past the last invocation ordinal that records. Read-only; set it through ordinalRange or configure.
var latestSnapshot: FieldProbeSnapshot? The most recent completed read-back, or null before the first one lands.
fun configure(samples: Int = sampleCapacity, binding: Int = this.binding, descriptorSet: Int = this.descriptorSet, enabled: Boolean = this.enabled, ordinalStart: UInt = this.ordinalStart, ordinalEndExclusive: UInt = this.ordinalEndExclusive): ShaderFieldProbe Sets several of the probe's settings at once and returns the probe, for configuring it in one named-argument call.
fun ordinalRange(start: UInt = 0u, endExclusive: UInt = UInt.MAX_VALUE): ShaderFieldProbe Records only the calls whose ordinal falls in [start, endExclusive), and returns the probe for chaining.
fun ordinalRange(range: UIntRange): ShaderFieldProbe The same as the two-argument ordinalRange, written as a Kotlin range, so 0u..4095u says what 0u, 4096u does.
fun onSnapshot(callback: (FieldProbeSnapshot) -> Unit): ShaderFieldProbe Hands every completed read-back to callback, and returns the probe for chaining.
fun display(channel: String = "probe"): ShaderFieldProbe Set the box output channel that receives probe summaries, and return this probe for chaining.
fun deregister(closeOutput: Boolean = true, clearOutput: Boolean = false): ShaderFieldProbe Turns the probe off and takes down what it registered: the display channel, the callback, the last snapshot and any read-back still queued.
fun readLatest(): FieldProbeSnapshot? The most recent completed read-back, or null before the first one. The same value as latestSnapshot, as a call for code that reads better that way.
fun readLatestScalarBatch(minSamples: Int = 1, clipLower: Double? = null, clipUpper: Double? = null, clipEpsilon: Double = 1.0e-6): FieldProbeScalarBatch? readLatest put through scalarBatch, so a controller can take the latest samples in one call.
fun readSnapshot(renderer: Renderer, imageIndex: Int): FieldProbeSnapshot Reads the probe buffer for one swapchain image and builds a snapshot from it, without waiting for the GPU.

FieldStats.kt

Computes streaming statistics of shader values on the GPU and reads the results back to box code.

ShaderFieldStats owns a partial buffer and a result buffer (default bindings FIELD_STATS_DEFAULT_PARTIAL_BINDING and FIELD_STATS_DEFAULT_RESULT_BINDING) for a shader that includes fieldStats. Each channel reduces to a FieldScalarStatsSnapshot, whose FieldScalarStatsSnapshot.quantile and FieldScalarStatsSnapshot.cdf read a fixed-size centroid sketch; ShaderFieldStats.scalar and ShaderFieldStats.state give per-channel and per-slot handles with read() and latest().

src/jvmMain/kotlin/afield/graphics/vulkan/util/FieldStats.kt — read the source

Used in: Field Probes: instrumenting shaders

Top-level values

Declaration Summary
const val FIELD_STATS_INCLUDE_NAME The name a shader includes to get the statistics GLSL: #include "fieldStats".
const val FIELD_STATS_PARTIAL_BUFFER_NAME The name of the scratch buffer the recording shader appends subgroup partials to, used to match the shader's declaration to the buffer the host allocates.
const val FIELD_STATS_RESULT_BUFFER_NAME The name of the buffer the reduce pass writes finished statistics into, and the only one the host reads back.
const val FIELD_STATS_DEFAULT_PARTIAL_BINDING Default binding for the partial buffer, 61, up beside the probe's 63, where a shader's own bindings are unlikely to reach. Override it on the constructor if they do.
const val FIELD_STATS_DEFAULT_RESULT_BINDING Default binding for the result buffer, 62, next door to FIELD_STATS_DEFAULT_PARTIAL_BINDING.

ShaderFieldStatsBinding

data class ShaderFieldStatsBinding(val stats: ShaderFieldStats, val producer: Boolean)

What a box remembers about the statistics block its shaders include: which block, and whether this box is the one recording into it.

Declaration Summary
val stats: ShaderFieldStats The statistics block itself, which stays owned by the box that allocated it however many consumers bind to it.
val producer: Boolean True for the box that records samples, which gets the recording functions and the subgroup extensions they need; false for a box that only reads the reduced result.

FieldScalarStatsSnapshot

data class FieldScalarStatsSnapshot(val generation: Long, val calls: Long, val finiteCount: Long, val invalidCount: Long, val nonZeroCount: Long, val min: Double, val max: Double, val mean: Double, val variance: Double, val sum: Double, val sumSquares: Double, val standardDeviation: Double, val droppedPartials: Long, val centroids: List<Pair<Double, Double>>)

One channel's statistics for one frame, as the reduce pass left them and the host read them back.

Declaration Summary
val generation: Long Counts completed reduce passes since the block was created, and so identifies the frame this came from. Two reads with the same generation are the same frame's numbers.
val calls: Long How many shader invocations recorded into this channel, finite or not.
val finiteCount: Long How many of those values were finite. This is the denominator for mean and variance, and the total weight the quantile sketch is read against.
val invalidCount: Long How many were NaN or infinite. Non-zero here is the signal to look for: it says the shader is producing values that no average will show.
val nonZeroCount: Long How many finite values were not exactly zero: the cheap test for whether a channel is carrying anything at all.
val min: Double The smallest finite value recorded, exactly, not read off the sketch. Positive infinity when nothing finite was recorded.
val max: Double The largest finite value recorded, exactly. Negative infinity when nothing finite was recorded.
val mean: Double The mean of the finite values, sum over finiteCount, and zero when there were none.
val variance: Double The population variance, computed on the GPU as the mean of squares less the square of the mean and clamped at zero. That form loses precision when the values are large and their spread is small, which is where the clamp earns its place.
val sum: Double The sum of the finite values.
val sumSquares: Double The sum of their squares.
val standardDeviation: Double The square root of variance.
val droppedPartials: Long How many subgroup partials the recording pass had to throw away because the partial buffer was full. Non-zero means every number here was computed from a subset, and the fix is a larger maxPartials.
val centroids: List<Pair<Double, Double>> The quantile sketch: value-and-weight pairs in ascending value order, weights summing to finiteCount, empty entries dropped. Read it through quantile and cdf rather than directly. A centroid's value is one recorded subgroup's mean standing in for a whole span of them, so the sketch summarises group averages rather than individual samples.
fun quantile(q: Double): Double The value at quantile q, read off the centroid sketch, with q clamped to 0..1.
fun cdf(value: Double): Double The fraction of recorded values at or below value, read off the centroid sketch and clamped to 0..1.

FieldStatsStateSnapshot

data class FieldStatsStateSnapshot(val mode: Int, val channel: Int, val parameter: Double, val halfLife: Double, val value: Double, val initialized: Boolean)

One smoothing slot's current reading: a statistic of a channel, carried across frames with exponential smoothing instead of being recomputed from scratch.

Declaration Summary
val mode: Int Which statistic is being smoothed: 0 for an unused slot, 1 for the mean, 2 for the quantile at parameter.
val channel: Int Which channel's statistic this slot follows.
val parameter: Double The quantile this slot tracks when mode is 2, between 0 and 1. Unread for the mean.
val halfLife: Double How many frames it takes the smoothed value to cover half the distance to a new reading. The per-frame blend is 1 - 2^(-1/halfLife), so larger is slower; the shader side floors it just above zero.
val value: Double The smoothed value as of this frame. Zero on a slot that has not run yet.
val initialized: Boolean Whether the slot has a real reading. A slot starts uninitialised and takes its first frame's statistic whole rather than blending up from zero, and a slot whose mode, channel, quantile or half-life changes restarts the same way, so a reconfigured slot does not drift from its old value to its new one.

ShaderFieldStatsScalar

class ShaderFieldStatsScalar internal constructor(private val stats: ShaderFieldStats, val index: Int)

A handle on one channel of a statistics block, so box code can hold onto a channel rather than passing its index around.

Declaration Summary
val index: Int Which channel this handle refers to.
fun read(): FieldScalarStatsSnapshot Reads the result buffer as it stands, without waiting for the GPU. See ShaderFieldStats.read for what that means for the numbers.
fun latest(): FieldScalarStatsSnapshot? The most recent fence-synchronised read-back for this channel, or null if none has completed yet. This is the one to call each frame.

ShaderFieldStatsState

class ShaderFieldStatsState internal constructor(private val stats: ShaderFieldStats, val index: Int)

A handle on one smoothing slot of a statistics block, obtained from ShaderFieldStats.state.

Declaration Summary
val index: Int Which slot this handle refers to.
fun read(): FieldStatsStateSnapshot Reads the result buffer as it stands, without waiting for the GPU.
fun latest(): FieldStatsStateSnapshot? The most recent fence-synchronised read-back for this slot, or null if none has completed yet.

ShaderFieldStats

class ShaderFieldStats(val box: Box, val channels: Int = 1, val sketchSize: Int = 256, val maxPartials: Int = 65_536, val stateSlots: Int = 8, val descriptorSet: Int = 0, val partialBinding: Int = FIELD_STATS_DEFAULT_PARTIAL_BINDING, val resultBinding: Int = FIELD_STATS_DEFAULT_RESULT_BINDING)

A statistics block: the two GPU buffers, the two compute passes that reset and reduce them, and the read-back that brings each frame's numbers to box code.

Declaration Summary
val box: Box The box that owns this block. Its name and id are what a failed read-back is reported against.
val channels: Int How many independent statistics this block tracks, 1 to 64. A channel is a slot the shader chooses when it records, not a component of a vector: four channels is how you watch four unrelated quantities.
val sketchSize: Int How many centroids the quantile sketch keeps per channel: a power of two from 16 to 256, and also the reduce pass's workgroup size. Smaller is a coarser FieldScalarStatsSnapshot.quantile and nothing else: the counts and moments are reduced over every partial whatever this is.
val maxPartials: Int How many subgroup partials one channel may record in a frame before the rest are counted as FieldScalarStatsSnapshot.droppedPartials. This is the term that dominates the partial buffer's size, at 48 bytes per partial per channel, and the block is double-buffered, so that figure is allocated twice over.
val stateSlots: Int How many smoothing slots the block carries, 1 to 64. Slots cost almost nothing and are configured from the shader, so the default leaves room for a few without asking.
val descriptorSet: Int The descriptor set both buffers are bound in.
val partialBinding: Int Binding for the partial buffer. Change it only if the shader's own bindings reach this far up.
val resultBinding: Int Binding for the result buffer.
var cpuSnapshotsEnabled: Boolean Whether each frame queues a read-back of the result buffer to the CPU. On by default.
fun scalar(index: Int): ShaderFieldStatsScalar A handle on channel index, for code that would rather hold a channel than an index. Throws for an index outside 0 until channels.
fun latest(channel: Int = 0): FieldScalarStatsSnapshot? The most recent fence-synchronised read-back for channel, or null if none has completed yet.
fun read(channel: Int = 0): FieldScalarStatsSnapshot Reads channel out of the result buffer immediately, without waiting for the GPU.
fun state(index: Int): ShaderFieldStatsState A handle on smoothing slot index. Throws for an index outside 0 until stateSlots.
fun latestState(index: Int): FieldStatsStateSnapshot? The most recent fence-synchronised read-back for slot index, or null if none has completed yet. The per-frame call for a smoothing slot.
fun readState(index: Int): FieldStatsStateSnapshot Reads slot index out of the result buffer immediately, with the same no-fence caveat as read.

FramePresentationLog.kt

A diagnostic log of swapchain presentation timing, written to a file while enabled.

FramePresentationLog records when each present call was made and returned, and when the presentation or scanout of that image was observed; FramePresentationLog.mark adds a labelled line to the same log.

src/jvmMain/kotlin/afield/graphics/vulkan/util/FramePresentationLog.kt — read the source

FramePresentationLog

object FramePresentationLog

A diagnostic log of swapchain presentation timing, written to a file while enabled.

Declaration Summary
var enabled: Boolean
var waitAfterPresent: Boolean
fun start(outputPath: String? = null, waitAfterPresent: Boolean = Options.framePresentationLogWaitAfterPresent, append: Boolean = false): String
fun stop()
fun mark(message: String)
fun addLine(message: String)

GpuNeighbourGrid.kt

A uniform spatial grid over a particle buffer, rebuilt on the GPU each frame with GpuRadixSort, so a compute shader can find a particle's neighbours by visiting nearby cells.

src/jvmMain/kotlin/afield/graphics/vulkan/util/GpuNeighbourGrid.kt — read the source

GpuNeighbourGrid

class GpuNeighbourGrid(val capacity: Int, val cellsPerSide: Int, var domainMin: Vector2f, var domainSize: Float, val renderer: Renderer = fromContext(), val name: String = "neighbourGrid")

A uniform spatial grid over a buffer of particles, built on the GPU every frame, so a compute shader can ask "who is near me" without looking at everybody.

Declaration Summary
val capacity: Int How many particles the grid is sized for.
val cellsPerSide: Int Cells along each axis. cellsPerSide * cellsPerSide cells in total.
var domainMin: Vector2f Lower corner of the gridded region, in whatever coordinates the positions are in.
var domainSize: Float Side length of the gridded region, in the same coordinates.
val renderer: Renderer
val name: String Distinguishes this grid's buffers and modules from another's.
val cellCount: Int Total cells.
val cellSize: Float The width of one cell — the largest interaction radius a 3x3 query covers.
val keys: FrameBufferGroup Cell index per particle, sorted after install runs.
val values: FrameBufferGroup Particle index per entry, carried along by the sort — the gather order.
val cellStart: FrameBufferGroup First entry of each cell's run in values.
val cellEnd: FrameBufferGroup One past the last entry of each cell's run. Equal to cellStart for an empty cell.
fun install(into: ComputeTarget, positions: FrameBufferGroup, count: Int, strideFloats: Int, offsetFloats: Int = 0) Installs the four stages into into, in order, under name.
fun install(into: MutableMap<String, Target>, positions: FrameBufferGroup, count: Int, strideFloats: Int, offsetFloats: Int = 0) As install, into any target's others map — a afield.graphics.vulkan.targets.RenderTarget's included.
fun install(into: MutableMap<String, Target>, positions: () -> FrameBufferGroup, count: () -> Int, strideFloats: Int, offsetFloats: Int = 0) As install, with the positions and the particle count read every frame, for a system whose count changes while it runs (particles painted in) or whose positions alternate between two buffers. A count of zero skips the frame; one above capacity is clamped to it.
fun target(positions: () -> FrameBufferGroup, count: () -> Int, strideFloats: Int, offsetFloats: Int = 0): Target The four stages as one Target, for a caller that sequences its own frame and wants the grid rebuilt at a particular point in it rather than as an entry in an others map. positions and count are read every frame, as in the live-count install.
fun removeFrom(into: ComputeTarget) Takes the grid back out of into.
fun removeFrom(into: MutableMap<String, Target>) Takes the grid back out of any target's others map.
fun cleanup()
fun glsl(prefix: String = "grid"): String GLSL declaring this grid's buffer-reference types and the two index helpers a consumer needs, plus a worked comment showing the 3x3 walk.

GpuProbe.kt

Runs a compute shader against fixture buffers, checks that it ran, and compares its read-back output with stated bounds.

GpuProbe is the facility; a probe whose shader never produced output throws GpuProbeNeverLanded.

src/jvmMain/kotlin/afield/graphics/vulkan/util/GpuProbe.kt — read the source

Top-level functions

Declaration Summary
inline fun <R> gpuProbe(renderer: Renderer, name: String = "gpuProbe", noinline pump: (() -> Unit)? = null, block: (GpuProbe) -> R): R Run block against a GpuProbe and close it however block ends.

GpuProbe

class GpuProbe(val renderer: Renderer, val name: String = "gpuProbe", val pump: (() -> Unit)? = null) : AutoCloseable

A GPU compute shader, put on a bench: fixture buffers, a dispatch chain, evidence that it ran, and numbers printed next to the bounds they were compared against.

Declaration Summary
val renderer: Renderer
val name: String
val pump: (() -> Unit)? One rendered frame. { window.pollEvents(); renderer.render() } from jvmMain; from a box, leave it null and drive stillWaiting from inside frames { } with the box's own frame().
fun fixture(name: String, floats: Int): FrameBufferGroup A buffer of floats floats, host-writable and host-readable, owned by this probe.
fun adopt(name: String, buffer: FrameBufferGroup, floats: Int): FrameBufferGroup Take a buffer this probe did not allocate under its wing, so it can be armed and read.
fun nameOf(buffer: FrameBufferGroup): String The name fixture or adopt gave this buffer, for messages.
fun sizeOf(buffer: FrameBufferGroup): Int How many floats fixture allocated, or 0 if this is not one of ours.
fun upload(buffer: FrameBufferGroup, from: FloatArray) Upload from to the front of buffer. Fails loudly rather than truncating.
fun upload(buffer: FrameBufferGroup, from: FloatBuffer) Upload from to the front of buffer, from its position to its limit.
fun fill(buffer: FrameBufferGroup, value: Float, floats: Int = sizeOf(buffer)) Write value into the first floats floats of buffer.
inner class Stage internal constructor(val stageName: String, internal val module: ComputeModulePointer, internal val key: String) One compute shader, registered on the renderer, with one or more dispatches behind it.
inner class Bindings internal constructor(private val stage: Stage, private val push: Structure, private val label: String) The receiver of Stage.dispatch's binding block.
val stageList: List<Stage> The stages created so far, in the order they run.
fun stage(stageName: String, glsl: String, configure: Parameters.() -> Unit = {}): Stage Compile glsl as a compute shader and register it, after every stage created before it.
fun arm(vararg outputs: FrameBufferGroup, seconds: Double = DEFAULT_LANDING_SECONDS) Prefill every outputs buffer with SENTINEL and start the clock.
fun landed(): Boolean Whether every armed output has been completely overwritten by the GPU.
fun stillWaiting(): Boolean !landed(), plus a deadline and a frame count — the loop condition.
val framesToLand: Int How many frames the caller pumped before everything landed, or -1 if it never did.
fun idle() Wait every fence this window has — "the GPU has nothing of mine left in flight".
fun settle(vararg outputs: FrameBufferGroup, seconds: Double = DEFAULT_LANDING_SECONDS) arm, then pump until landed — the whole wait, when this probe owns a pump.
fun settleUntil(vararg outputs: FrameBufferGroup, seconds: Double = DEFAULT_LANDING_SECONDS, readyLabel: String = "the caller's readiness condition", ready: () -> Boolean) settle, and then keep pumping until ready holds as well.
fun pumpFrames(n: Int) Render n frames. Needs pump, which means jvmMain.
fun read(buffer: FrameBufferGroup, floats: Int = sizeOf(buffer)): FloatArray Read the first floats floats of buffer back to the host.
fun readInts(buffer: FrameBufferGroup, ints: Int = sizeOf(buffer)): IntArray Read the first ints ints of buffer back to the host.
fun fail(message: String) Record an unconditional failure — a shader that did not compile, an exception, a gap.
fun atLeast(label: String, measured: Number, floor: Number, buffer: String? = null, index: Int? = null) measured must be at least floor. Both numbers go in the verdict table.
fun atMost(label: String, measured: Number, ceiling: Number, buffer: String? = null, index: Int? = null) measured must be at most ceiling.
fun exactly(label: String, measured: Number, expected: Number, buffer: String? = null, index: Int? = null) measured must equal expected exactly — counts, indices, set sizes.
fun varies(label: String, values: FloatArray, floor: Number, buffer: String? = null) The spread of values must be at least floor — the guard against a probe that passes because the GPU wrote a constant, or nothing but zeros.
fun allFinite(label: String, values: FloatArray, buffer: String? = null) Every one of values must be finite, and none may still be SENTINEL.
fun verdict(title: String = name, print: (String) -> Unit = ::println): Int Print every claim with its measured value beside its bound, and return the failure count.
fun verdictLines(title: String = name): List<String> verdict's table as lines, for a caller that wants to put it somewhere other than a stream — a box display channel, a markdown fence, a test's assertion message.
override fun close() Remove every module this probe put on the renderer.

GpuProbeNeverLanded

class GpuProbeNeverLanded(message: String) : RuntimeException(message)

Thrown by GpuProbe.stillWaiting when the armed outputs are never completely written.

GpuRadixSort.kt

A stable GPU radix sort of uint keys in storage buffers, with an optional uint payload carried along.

GpuRadixSort owns its scratch buffers and compute shaders. asFrameBufferGroup wraps a single VulkanBuffer so it can be passed where a per-frame buffer group is expected.

src/jvmMain/kotlin/afield/graphics/vulkan/util/GpuRadixSort.kt — read the source

Top-level functions

Declaration Summary
fun VulkanBuffer.asFrameBufferGroup(renderer: Renderer = this.renderer): FrameBufferGroup

GpuRadixSort

class GpuRadixSort(val renderer: Renderer = fromContext(), val workgroupSize: Int = 256, val bitsPerPass: Int = 4)

Stable GPU radix sort for uint keys stored in storage buffers.

Declaration Summary
val renderer: Renderer
val workgroupSize: Int
val bitsPerPass: Int
data class SortResult(val keys: FrameBufferGroup, val values: FrameBufferGroup?)
fun sort(commandBuffer: CommandBuffer, keys: FrameBufferGroup, count: Int, values: FrameBufferGroup? = null, signedKeys: Boolean = false): SortResult+1 overload · also VulkanBuffer, VulkanBuffer?
fun installInto(target: RenderTarget, keys: FrameBufferGroup, count: Int, values: FrameBufferGroup? = null, signedKeys: Boolean = false, name: String = "gpuRadixSort")+1 overload · also VulkanBuffer, VulkanBuffer?
fun installInto(target: RenderTarget, keys: FrameBufferGroup, count: Int, values: FrameBufferGroup? = null, signedKeys: Boolean = false, name: String = "gpuRadixSort", onSorted: (SortResult) -> Unit)+1 overload · also VulkanBuffer, VulkanBuffer?
fun reserve(count: Int, withValues: Boolean = true) Allocates scratch for sorting up to count keys now, rather than on the first sort that needs it. A sort whose count grows a little every frame (particles being painted in) would otherwise reallocate its scratch on every one of those frames, and the old scratch is only freed with the renderer.
fun cleanup()

ImageReducer.kt

Reduces an image to global summary statistics on the GPU in two compute passes.

ImageReducer reads the source as a sampled texture, so the image needs the sampled usage bit and its layout is left alone; results come back through ImageReducer.readResults.

src/jvmMain/kotlin/afield/graphics/vulkan/util/ImageReducer.kt — read the source

ImageReducer

class ImageReducer(val renderer: Renderer = fromContext(), private val imageView: HasImageView)

ImageReducer - reduces an image (HasImageView) to global summary statistics using two Vulkan compute-shader passes.

Declaration Summary
val renderer: Renderer
data class ReductionResult(val min: FloatArray, val avg: FloatArray, val max: FloatArray, val lowAvgLuma: Float, val highAvgLuma: Float, val lowAvgRgb: FloatArray, val highAvgRgb: FloatArray, val midSatAvg: Float, val contrastRange: Float, val contrastMichelson: Float, val lowFraction: Float, val highFraction: Float, val midFraction: Float) Data class to hold the reduction results
fun reduce(commandBuffer: CommandBuffer) Execute the reduction on the GPU. Must be called within a command buffer recording.
fun readResults(): ReductionResult Read back the results from the GPU. This should be called after GPU execution has completed (e.g., after fence wait).
fun cleanup() Clean up GPU resources
fun reduceNow(): ReductionResult Execute the reduction once and return the results synchronously. This is a convenience method that creates a command buffer, executes the reduction, waits for completion, and returns the results.
fun installInto(target: RenderTarget, name: String = "imageReducer") Install this reducer into a RenderTarget's others map for per-frame execution. The reduction will be executed every frame as part of the render target's rendering. Results can be read back asynchronously after the frame completes.
fun installInto(target: RenderTarget, name: String = "imageReducer", onResults: (ReductionResult) -> Unit) Install this reducer into a RenderTarget with a callback that receives the results. The callback is invoked after each frame's reduction completes. Note: The callback is invoked synchronously during rendering, so keep it fast.

KeyCamera.kt

Keyboard camera controls for a Vulkan window.

KeyCamera registers a poll callback on the renderer that moves a Camera from held keys; keyCamera and KeyCameraDelegateProvider create one named after the property it is assigned to, so re-running a box replaces its controls rather than adding more.

src/jvmMain/kotlin/afield/graphics/vulkan/util/KeyCamera.kt — read the source

Used in: Field 4 Graphics

Top-level functions

Declaration Summary
fun keyCamera(renderer: Renderer = fromContext(), camera: Camera = fromContext()): KeyCameraDelegateProvider Factory function to create a delegate for KeyCamera using the property's name.

KeyCamera

class KeyCamera(val renderer: Renderer = fromContext(), val camera: Camera = fromContext(), val name: String = "__keycamera__", private val acceptPhysicalKeys: Boolean = true)

Binds keyboard camera controls into the current renderer poll loop.

Declaration Summary
val renderer: Renderer The renderer whose window is read for held keys and whose poll loop runs the controls.
val camera: Camera The camera the keys move. R puts its position, target, up vector and field of view back to what they were when this KeyCamera was built, along with decay, movementSpeed and rotationSpeed.
val name: String The key this registration takes in the window's poll map. Building a second KeyCamera under the same name replaces the first rather than adding a second set of controls, which is how re-running a box leaves one.
var actions The motion still owed, as action key to amount and what to do with it. Holding a key sets its amount back to 1 every poll; each poll then decays the amount, calls the action with it, and drops the entry once it falls below 0.01 — which is the glide that carries on after the key comes up. Adding an entry here moves the camera exactly as a key would.
var perActionDecays The decay in force for each action, for overriding decay on one of them. An action with no entry takes decay the next time it runs, and pressing its key clears the entry, so a value set here governs the glide of a key that is already down.
var decay How much of an action's amount survives one 60 Hz frame, raised to the power of the frame's share of 1/60 s so the glide lasts the same wall-clock time at any frame rate. Lower stops sooner; 1 never stops.
var movementSpeed How far one 60 Hz frame of a movement key travels, as a fraction of the camera's distance to its target, so the same keystroke moves less the closer you are. Turning has its own rotationSpeed.
var rotationSpeed Scale for keyboard yaw, pitch and roll; larger values turn the camera faster.

KeyCameraDelegateProvider

class KeyCameraDelegateProvider(private val renderer: Renderer = fromContext(), private val camera: Camera = fromContext(), private val box: Box = fromContext())

Delegate provider that creates a KeyCamera whose name equals the property name.

Declaration Summary
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadWriteProperty<Any?, KeyCamera>

KeyEvents.kt

Keyboard handlers for a Vulkan window, polled with the renderer.

KeyEvents registers handlers per key; a down handler's DownResult says whether it stays registered, runs a callback on the matching release, or starts a per-frame generator that runs while the key is held.

src/jvmMain/kotlin/afield/graphics/vulkan/util/KeyEvents.kt — read the source

DownResult

sealed class DownResult

Result contract for "down" handlers.

Declaration Summary
data object Keep : DownResult() Keep the handler registered (default behavior).
data object Remove : DownResult() Unregister the handler after this down event.
data class OnUp(val onUp: () -> Unit) : DownResult() Register a one-shot callback that will be invoked the next time the same key/button goes UP (i.e., the matching release for this specific press). The handler itself remains registered.
data class WhileDown(val generator: Generator<Boolean, Boolean>) : DownResult() Register a per-frame generator that will be stepped every poll. The Boolean input is true while the key is down, false after release. The generator is driven until completion.

KeyEvents

class KeyEvents(val renderer: Renderer = fromContext(), val name: String = "__keyevents__")

Declaration Summary
fun onDownAndUp(key: String, name: String, callback: () -> DownResult)
fun onDownUntilUp(key: String, name: String, onUp: () -> Unit)
fun onDown(key: String, name: String, callback: () -> Boolean)
fun onUp(key: String, name: String, callback: () -> Boolean)

MapOfSequences.kt

Keyed collections of running sequences that a host steps once per frame.

MapOfSequences holds the Runloop's preRun/postRun hooks and a mesh's update map, and can record each hook in the lifecycle ledger; MapOfGenerators holds generators that take an input each step and yield a value, without ledger entries.

src/jvmMain/kotlin/afield/graphics/vulkan/util/MapOfSequences.kt — read the source

MapOfSequences

class MapOfSequences<T>(private val ledgerKind: String? = null)

Named sequences advanced one step per update — the Runloop's preRun/postRun hooks, a mesh's update. Setting a key replaces whatever ran under it; a sequence that ends is dropped.

Declaration Summary
fun update()
operator fun set(t: T, sequence: Sequence<Any?>) Run sequence under t from the next update, replacing whatever ran there.
fun setUnowned(t: T, sequence: Sequence<Any?>) set without the ledger entry, for library code that installs a hook on its own schedule and removes it itself — a renderer's geometry hook, a spreadsheet poll — rather than on a box's behalf.
fun isCurrent(t: T, sequence: Sequence<Any?>): Boolean Whether sequence is still what runs, or will run, under t.
fun removeIfCurrent(t: T, sequence: Sequence<Any?>) Remove t only if sequence is still what it holds.
fun remove(t: T)

MapOfGenerators

class MapOfGenerators<O, T>

Declaration Summary
val starts
var ongoings
fun update(input: O): MutableList<T>
fun updateNoCollect(input: O)
operator fun set(t: String, g: Generator<O, T>)
fun remove(t : String)

MouseEvents.kt

Mouse input from a Vulkan renderer's window, delivered to named callbacks.

MouseEvents registers handlers for down, up, move, drag and scroll, keyed by a name so that re-running a box replaces its handler rather than adding another. Each handler receives a MouseEvent with position, delta, scroll, buttons held, and the window size.

src/jvmMain/kotlin/afield/graphics/vulkan/util/MouseEvents.kt — read the source

MouseEvent

data class MouseEvent(val pos: Vec2, val delta: Vec2 = Vec2(), val scroll: Vec2 = Vec2(), val buttonsDown: Set<String> = emptySet(), val button: String? = null, val type: Type, val windowSize: Vec2 = Vec2(1f, 1f))

Mouse input from a Vulkan renderer's window, delivered to named callbacks.

Declaration Summary
val pos: Vec2 Where the pointer is, in points from the window's top-left corner — the same units as windowSize, not framebuffer pixels.
val delta: Vec2 How far the pointer moved since the previous poll, in the same points. Only Type.MOVE and Type.DRAG carry one; a press, release or scroll arrives with zero.
val scroll: Vec2 Wheel and trackpad movement since the previous poll, horizontal then vertical. Only a Type.SCROLL event carries one.
val buttonsDown: Set<String> The buttons held at this poll, under the names left, right, middle and the GLFW numbers. Only buttons some handler asked about are read, so one with no onDown, onUp or per-button onDrag never appears here however hard it is held. An onDrag with no button named asks for left, right and middle.
val button: String? Which button this event is about. A press, a release and a per-button drag name one; a move, a scroll and an any-button drag leave it null.
val type: Type Which of the five kinds of event this is.
val windowSize: Vec2 The window's size, in the same units as pos: window points, not framebuffer pixels.
enum class Type
fun isDown(name: String)
val uv: Vec2 pos as a fraction of the window: (0, 0) at its top-left corner, (1, 1) at its bottom-right.

MouseEvents

class MouseEvents(val renderer: Renderer = fromContext(), val name: String = "__mouseevents__")

Declaration Summary
val renderer: Renderer The renderer whose window is polled. This becomes that window's mouseEvents if it does not already have one.
val name: String The key this polling takes in the window's poll map, which is not the name a handler is registered under. A second MouseEvents built with the same name takes over the polling from the first, so re-running a box leaves one; the name passed to onDown, onMove and the rest is what keeps a box from stacking up duplicate handlers inside it.
fun onDown(button: String, name: String, callback: (MouseEvent) -> DownResult)+1 overload · also () -> Boolean
fun onDownUntilUp(button: String, name: String, onUp: (MouseEvent) -> Unit) Handy helper to attach a one-shot onUp for this button press.
fun onUp(button: String, name: String, callback: (MouseEvent) -> Boolean)+1 overload · also () -> Boolean
fun onMove(name: String, callback: (MouseEvent) -> Boolean)
fun onDrag(name: String, callback: (MouseEvent) -> Boolean) Calls callback whenever the mouse moves with any of the left, right or middle buttons down. Return false to remove it. Needs no onDown beside it.
fun onDrag(button: String, name: String, callback: (MouseEvent) -> Boolean) Calls callback whenever the mouse moves with button down. Return false to remove it.
fun onScroll(name: String, callback: (MouseEvent) -> Boolean)
fun remove(name: String) Remove only the callbacks installed under this name, leaving other tools' input intact.

Options.kt

Global switches for the Vulkan renderer, mostly read from system properties at startup, and the code that locates the Vulkan loader and driver.

Options holds the platform flags, profiling and frame-presentation logging toggles, debug naming and labels, and VR. On macOS, Options.doVulkanSDK uses the loader named by AFIELD_VULKAN_LIBRARY, else VULKAN_SDK, else the newest ~/VulkanSDK version, falls back to LWJGL's bundled MoltenVK when none exists, and pins the driver chosen by Options.macVulkanDriver.

src/jvmMain/kotlin/afield/graphics/vulkan/util/Options.kt — read the source

Options

object Options

Global switches for the Vulkan renderer, mostly read from system properties at startup, and the code that locates the Vulkan loader and driver.

Declaration Summary
val isWindows Running on Windows, by os.name. The three platform flags are how the rest of the renderer branches, rather than each asking the JVM again.
val isLinux Running on Linux, by os.name.
val isMac Running on macOS, by os.name. This is the flag that gates the loader and driver work in doVulkanSDK, and the default for unifiedImageLayouts.
val target The native classifier for this OS: macosx, win-x64 or linux-x64. Nothing reads it, but it is not inert: it is an eager val, so first touching Options on an OS that is none of the three throws rather than running on without a platform.
enum class MacVulkanDriver(val icdManifest: String, val driverId: Int) The macOS Vulkan driver (ICD) Field asks the loader for. The SDK ships two, and they differ in ways Field's code can see — see dev_docs/KOSMICKRISP.md.
var macVulkanDriver: MacVulkanDriver Which macOS driver to ask the loader for, from afield.four.vulkanDriver or AFIELD_VULKAN_DRIVER — auto, moltenvk or kosmickrisp, with auto the default and an unrecognised name an error when Options loads. resolvedMacVulkanDriver is the same question with auto answered, and is what doVulkanSDK pins.
val resolvedMacVulkanDriver: MacVulkanDriver macVulkanDriver with AUTO decided.
val kosmicKrispCanRun: Boolean by lazy Apple silicon, macOS 26+ (KosmicKrisp is built on Metal 4), and an SDK of at least KOSMICKRISP_MIN_SDK that ships the driver. Earlier SDKs ship a KosmicKrisp that is not ready for Field: 1.4.335's lacked vertex-pipeline stores, so vkCreateDevice refused Field's features. An SDK whose directory names no version (a hand-set VULKAN_SDK) is taken at its word.
val KOSMICKRISP_MIN_SDK The first SDK whose KosmicKrisp Field's GPU smoke catalog passed on (1.4-conformant).
val macVulkanSdk: File? by lazy The macOS SDK root (…/VulkanSDK/<version>/macOS) whose loader Field uses: VULKAN_SDK when set, else the newest version under ~/VulkanSDK. Null when there is none, in which case LWJGL loads its own bundled MoltenVK directly, with no loader, and KosmicKrisp cannot be reached at all.
var vulkan_library_name: String? The loader doVulkanSDK hands LWJGL: afield.four.vulkanLibrary or AFIELD_VULKAN_LIBRARY when either is set, else the libvulkan.1.dylib inside macVulkanSdk. A path that does not exist is ignored, and setting it after Vulkan has loaded changes nothing.
val useVulkanSDK Whether to hand LWJGL the SDK loader at all. Constant, and read only by doVulkanSDK: turning it off would leave macOS on LWJGL's bundled MoltenVK.
val robustness Whether the device asks for robustBufferAccess, which bounds-checks shader buffer reads at some cost. Off: Field's shaders index within bounds and the smoke catalog is the check.
var profiling Collect per-frame GPU timestamps, which the renderer's profiler reports. afield.four.profiling or AFIELD_FOUR_PROFILING, off by default, and also set from the user's preferences at startup.
var framePresentationLog Start the frame-presentation log with the process rather than when something asks for it — afield.four.framePresentationLog or AFIELD_FOUR_FRAME_PRESENTATION_LOG. From a box, frameLogStart and frameLogStop start and stop the same log without a restart, which is what to reach for when frames are janky.
var framePresentationLogPath: String? Where to write that log. afield.four.framePresentationLogPath or AFIELD_FOUR_FRAME_PRESENTATION_LOG_PATH; null or blank lets FramePresentationLog choose a timestamped path and report it.
var framePresentationLogWaitAfterPresent Where the swapchain's wait for a present goes, which changes what the log measures rather than how it reports it. Off, each acquire first waits for the previous present, so a frame's presentation is observed a frame late; on, every present is waited for as soon as it is made, which dates it to the frame it belongs to and costs throughput. afield.four.framePresentationLogWaitAfterPresent or AFIELD_FOUR_FRAME_PRESENTATION_LOG_WAIT_AFTER_PRESENT.
var framePresentationLogFlushEvery How many lines the log buffers before flushing to disk. Default 120, floored at 1; 1 costs a write per line and is what to use when the run may end in a crash.
var framePresentationLogPresentWaitTimeoutNs How long vkWaitForPresentKHR is given, in nanoseconds. Default 50 ms, floored at 1 ns; a wait that times out warns and the frame goes on, so this bounds a stalled compositor rather than tuning anything.
var presentTimingQueueSize How many past-presentation-timing slots the swapchain reserves for VK_EXT_present_timing. Default 8, floored at 1. Raise it if a high-frequency drain misses entries. afield.four.presentTimingQueueSize or AFIELD_FOUR_PRESENT_TIMING_QUEUE_SIZE.
var logShadercSource Log the line-numbered GLSL handed to shaderc, on the debug,graphics channel, for every stage compiled. On by default, since a compiler error names a line in the assembled source rather than in anything you wrote. afield.four.logShadercSource or AFIELD_FOUR_LOG_SHADERC_SOURCE.
var computeOncePerFrame Record the compute pass once per frame rather than once per window. On by default: a simulation stepped per window advances two or three times a frame when the same document has several windows open on it. afield.four.computeOncePerFrame or AFIELD_FOUR_COMPUTE_ONCE_PER_FRAME.
var unifiedImageLayouts Ask for VK_KHR_unified_image_layouts where the device offers it, which lets every layout but UNDEFINED, PREINITIALIZED and PRESENT_SRC go to Vulkan as GENERAL (ImageLayouts). On by default on macOS only, where the GPU sweep has run with it; elsewhere it is afield.four.unifiedLayouts or AFIELD_FOUR_UNIFIED_LAYOUTS=true.
var oneTimeSubmitFrames Record the renderer's per-frame command buffers with ONE_TIME_SUBMIT, telling the driver each is submitted once and can be discarded after. On by default, and a switch rather than a constant so the gain can be measured on a driver without a rebuild. afield.four.oneTimeSubmit or AFIELD_FOUR_ONE_TIME_SUBMIT.
var secondaryGraphicsCommandBuffers Record a window's graphics work into secondary command buffers, so several can be recorded in parallel. Asked for by default, and used only where Device.supportsMaintenance7 also holds, so on a device without that extension this reads true and nothing changes. afield.four.secondaryGraphicsCommandBuffers or AFIELD_FOUR_SECONDARY_GRAPHICS_COMMAND_BUFFERS.
var captureCompatibility Run as a GPU capture tool expects: it clears validate, dumpToolingInfo, debugObjectNames, debugLabels and shaderDebugPrintf, every one of which either injects a layer of its own or annotates the stream a capture is trying to read. afield.four.captureCompatibility or AFIELD_FOUR_CAPTURE_COMPAT.
var dumpToolingInfo Report the tooling layers attached to the device, which answers "is a capture tool in this stream". Off by default, because the call that asks — vkGetPhysicalDeviceToolPropertiesEXT — runs inside whatever layer injected itself, and under Nsight that has taken the JVM down at startup. afield.four.dumpToolingInfo or AFIELD_FOUR_DUMP_TOOLING_INFO, and captureCompatibility holds it off.
var debugObjectNames Attach names to Vulkan handles, so a validation message or a RenderDoc capture says which buffer rather than which address. On by default, and inert unless validate is on and the driver exposes VK_EXT_debug_utils. afield.four.debugObjectNames or AFIELD_FOUR_DEBUG_OBJECT_NAMES.
var debugLabels Bracket regions of a command buffer with labels, which is what gives a frame debugger its nesting. Same conditions as debugObjectNames, and additionally skipped on macOS. afield.four.debugLabels or AFIELD_FOUR_DEBUG_LABELS.
var enableVR Bring up OpenXR alongside Vulkan: the instance and device ask for the runtime's required extensions, the renderer creates an XR session, and the swapchain picks a format the runtime will take. Off by default and set from the user's preferences at startup, so it is read before the instance exists and changing it afterwards does not reach one.
fun doVulkanSDK() Choose the Vulkan loader and driver. Must run before LWJGL first loads Vulkan, after which VULKAN_LIBRARY_NAME is ignored. Window and Instance call it themselves, because an entrypoint that does not silently gets LWJGL's bundled MoltenVK: no loader, no validation layers, no KosmicKrisp. Repeat calls do nothing.
var defaultDepthFormat: Int The depth format new depth attachments are created with. D32_SFLOAT until chooseDefaultDepthFormat has asked a physical device, after which it is the first of D32_SFLOAT, D16_UNORM, D24_UNORM_S8_UINT, D32_SFLOAT_S8_UINT that device can use as a depth attachment — unless afield.four.depthFormat or AFIELD_FOUR_DEPTH_FORMAT named one (D16, D32, D24S8, D32S8), which pins it with no support check at all.
fun chooseDefaultDepthFormat(physicalDevice: VkPhysicalDevice): Int
fun depthHasStencil(format: Int): Boolean
fun depthAspectMask(format: Int): Int
fun depthFormatName(format: Int): String
val validateOverride: Boolean? What afield.validate or AFIELD_VALIDATE asked for, or null when neither is set to a strict true or false. Kept apart from validate so that "nobody said" is distinguishable from "someone said no": the server takes the user's preference only in the null case, so a command-line answer outranks the preference without having to be re-stated.
var validate Ask the instance for VK_LAYER_KHRONOS_validation. On in a bare entrypoint, and under the server whatever validateOverride says, or the user's preference when it says nothing; captureCompatibility clears it either way.
var validateOpenXR Separate from Vulkan validation: vendor OpenXR extensions can outpace installed API layers.
var shaderDebugPrintf Enable debugPrintfEXT in shaders without turning all of validate on, since the printf plumbing rides on the same validation layer. Off by default; with neither this nor validate on, a shader's printf compiles and prints nothing, and the compiler warns once that it will. afield.four.shaderDebugPrintf or AFIELD_FOUR_SHADER_DEBUG_PRINTF.
var preferredDevice: String? Pick a physical device by its exact reported name rather than by the default rule. Null, and nothing sets it from the environment, so this is for code that has read the device names and wants one of them. A name that matches nothing, or matches a device missing a required extension, is not an error: selection falls through to the default, which on macOS is the device belonging to the driver resolvedMacVulkanDriver names and elsewhere the first that qualifies.
var enableRayTracing: Boolean Ask the device for the ray-tracing pipeline and acceleration-structure extensions. On, and skipped on a device that does not offer them, so the cost of leaving it on is a feature query.
var requestedImages How many swapchain images to ask for, 3 by default. It is an upper bound rather than a count: the swapchain clamps the request to the surface's maximum and then raises it to the surface's minimum, and a surface that reports no maximum drops the request altogether and gives its minimum, which is usually 2. A window that will not triple-buffer is generally that, rather than a driver refusing.
var vsync Prefer a present mode that waits for vertical blank. On, which on macOS is FIFO, on Windows mailbox and on Linux relaxed FIFO; off puts immediate first, which tears and is what a frame-rate measurement wants. enableVR overrides it either way, because the desktop window is then a mirror on the same loop as xrWaitFrame and a blocking desktop present would cap the headset at the monitor's rate.
var maxFrameRate: Double Upper bound on renderer frames per second; 0 (the default) leaves the loop uncapped. A window the compositor is not showing is not throttled by vsync and can run at thousands of frames a second, so anything measured per frame (motion vectors, frame-to-frame displacement) shrinks towards zero. A capture that shows per-frame motion sets this, in its renderer box or through AFIELD_MAX_FRAME_RATE.
val fov: Double Default vertical field of view, in radians — Projection and SuperProjection hand it straight to setPerspective(fovy, …) and tan(fov / 2) respectively.
val zNear: Double Default near clip distance, in world units, which every Projection and SuperProjection starts from. 0.001 is close enough that a camera a metre from its subject does not clip it, and near enough zero to spend most of the depth buffer's precision on the first few units; push it out if depth fighting matters more than close geometry.
val zFar: Double Default far clip distance, in world units. 1000, with zNear giving the ratio the depth buffer's precision follows from.
var openXRClearColor: FloatArray The colour the OpenXR target clears each eye to, as RGBA. Opaque black. The desktop window has its own clear colour and does not read this.
var enableHDR: Boolean Prefer an HDR surface format — scRGB or HDR10 — over the default 8-bit sRGB, which needs VK_EXT_swapchain_colorspace on the instance and a display advertising an HDR colorspace. Where either is missing the swapchain is sRGB and nothing says so, so check the chosen surface format rather than this flag.
var openXRDebugInput: Boolean Print what the OpenXR runtime bound: interaction profiles and the sources behind each action. On, since a controller doing nothing is usually a profile that did not bind.
var openXRWaitForGpuBeforeRelease: Boolean Wait for the GPU to finish rendering into an XR swapchain image before releasing it. Off: the frame is submitted and released at once, which is what most runtimes expect and is much the faster of the two, but it leaves the runtime's own synchronisation to be relied on.

OrbitControls.kt

Mouse orbit controls for a Camera in a Vulkan window: rotate, pan and dolly around the camera's target, after Three.js's OrbitControls.

src/jvmMain/kotlin/afield/graphics/vulkan/util/OrbitControls.kt — read the source

Top-level functions

Declaration Summary
fun orbitControls(renderer: Renderer = fromContext(), camera: Camera = fromContext()): OrbitControlsDelegateProvider Factory function to create a delegate for OrbitControls using the property's name.

OrbitControls

class OrbitControls(val renderer: Renderer = fromContext(), val camera: Camera = fromContext(), mouseEvents: MouseEvents? = renderer.window.mouseEvents, val name: String = "__orbitcontrols__")

Mouse-driven orbit controls inspired by Three.js OrbitControls.

Declaration Summary
val renderer: Renderer The renderer whose window supplies the mouse events, the key state for the drag modifiers and the poll loop that damping runs in.
val camera: Camera The camera these controls move. Its at, target and up are written on every update.
val name: String The name the mouse handlers and the poll-loop entry are registered under, so a second set of controls on the same window does not displace the first.
val mouse: MouseEvents The mouse stream these controls listen on: the window's own MouseEvents when the constructor was given one, otherwise a private stream named <name>__mouse__.
var enabled Whether drags and scrolls move the camera at all. While false the handlers return early and damping stops advancing, so the camera holds its pose.
var enableRotate Whether a plain left-drag orbits the camera around the camera's target.
var enablePan Whether a right-drag, or shift and left-drag, slides camera and target together.
var enableZoom Whether the scroll wheel, a middle-drag and ctrl (or alt) with a left-drag change the distance to the target.
var rotateSpeed Radians of orbit per pixel dragged, as a multiple of a full turn across the window: at 1.0, dragging the window's own height turns 360° about either axis.
var zoomSpeed How sharply scrolling and dollying change the distance to the target: an event of d units multiplies it by 0.95 raised to zoomSpeed * d / 100, inwards or outwards according to the sign.
var panSpeed Pan distance per pixel dragged, as a multiple of what the camera can see: at 1.0, dragging the window's own height moves the target by the full visible height at the target's distance, so the scene keeps pace with the pointer.
var enableDamping Whether a drag or scroll leaves momentum behind. While it is set, update keeps being called from the window's poll loop after the mouse stops, each call applying and then shrinking what is left, until the pending angles are under a millionth of a radian and the pending pan under a thousandth of a world unit.
var dampingFactor The fraction of the pending orbit, pan and zoom that each damped update discards: 0.1 keeps nine tenths of the momentum per frame, 1.0 spends it all in one frame, and 0.0 never settles. Read only while enableDamping is set.
var minDistance The closest the camera may come to its target; the distance is clamped into this range by every update, not only by the drag that crossed it.
var maxDistance The farthest the camera may sit from its target, infinite by default; clamped like minDistance.
var minPolarAngle The smallest angle, in radians, between upAxis and the direction from target to camera: 0 is directly overhead, and the camera is held a millionth of a radian inside the limit so the pole itself stays unreachable.
var maxPolarAngle The largest such angle, PI (directly underneath) by default; held just inside the limit like minPolarAngle, which is what keeps the picture from flipping over at the pole.
var screenSpacePanning Which plane a pan moves the target in. Set, the drag moves it in the plane of the screen, along the camera's own up; clear, it moves in the plane that upAxis is normal to, so a vertical drag travels along the ground rather than lifting the target off it.
val upAxis The axis the camera orbits about and the one the polar limits are measured from: the camera's up as it stood when the controls were built, normalized. Every update writes it back into the camera, so the horizon stays level however far the orbit has travelled.
fun saveState() Records the camera's present position, target, up and upAxis as the pose reset returns to. The constructor records it once, so a reset with no save behind it returns the camera to where it stood when the controls were built.
fun reset() Puts the camera back to the pose saveState recorded and drops whatever orbit, pan or zoom was still pending.
fun update(): Boolean Applies the pending orbit, pan and zoom to the camera, clamping the distance and the polar angle on the way, and returns whether damping has left anything worth another call — which is what keeps the poll loop going while the camera coasts to a stop. The drag and scroll handlers call it themselves; with nothing pending it still clamps, so it also brings a camera that other code has moved back inside the limits.

OrbitControlsDelegateProvider

class OrbitControlsDelegateProvider(private val renderer: Renderer = fromContext(), private val camera: Camera = fromContext(), private val box: Box = fromContext())

Delegate provider that creates OrbitControls whose name equals the property name.

Declaration Summary
operator fun provideDelegate(thisRef: Any?, property: kotlin.reflect.KProperty<*>): kotlin.properties.ReadWriteProperty<Any?, OrbitControls>

Projection.kt

Projection matrices for the Vulkan renderer: a plain perspective Projection and a SuperProjection that blends perspective into orthographic.

SuperProjection is what Camera uses. Its orthoness runs from 0 (perspective) to 1 (orthographic) along a matrix-log path, with matchDepth (or the camera's distance to its target) as the depth where the two footprints agree, and ox/oy shifting the principal point.

The remaining functions are the matrix tools behind that blend: matrix4fToSimple and simpleToMatrix4f convert to EJML, expmEigen and logmEigen take the matrix exponential and logarithm, lieInterpolateProjectionEJML interpolates two projections, and renormDepth01 and renormVerticalFovAtDepth rescale a matrix's depth range and its field of view at a chosen depth.

src/jvmMain/kotlin/afield/graphics/vulkan/util/Projection.kt — read the source

Top-level functions

Declaration Summary
fun matrix4fToSimple(m: Matrix4f): SimpleMatrix
fun simpleToMatrix4f(s: SimpleMatrix): Matrix4f
fun expmEigen(A: SimpleMatrix): SimpleMatrix Matrix exponential via eigen decomposition: A = V * diag(λ) * V^{-1} exp(A) = V * diag(exp(λ)) * V^{-1}
fun logmEigen(A: SimpleMatrix): SimpleMatrix Matrix logarithm via eigen decomposition (real principal log): A = V * diag(λ) * V^{-1} log(A) = V * diag(log(λ)) * V^{-1}
fun lieInterpolateProjectionEJML(P0: Matrix4f, P1: Matrix4f, t: Float): Matrix4f Lie-group interpolation in GL(4, R) between P0 and P1:
fun Matrix4f.renormDepth01(near: Float, far: Float): Matrix4f Remap depth so that: z = -near → z_ndc = 0 z = -far → z_ndc = 1
fun Matrix4f.renormVerticalFovAtDepth(referencePersp: Matrix4f, zRef: Float, sampleY: Float = 1f, weight: Float = 1f): Matrix4f Isotropic FOV renormalization at a given depth zRef.

Projection

class Projection : HasUniform<Matrix4f>

Projection matrices for the Vulkan renderer: a plain perspective Projection and a SuperProjection that blends perspective into orthographic.

Declaration Summary
val projectionMatrix: Matrix4f The matrix itself, rebuilt in place by resize and returned by evaluateUniform. Editing fov, zNear or zFar does not touch it until the next resize.
var fov Vertical field of view in radians, and it must be positive.
var zNear Distance to the near clip plane, in view-space units; depth maps to 0 here. Defaults to Options.zNear.
var zFar Distance to the far clip plane, where depth maps to 1 — Vulkan's [0,1] range rather than OpenGL's [-1,1]. Defaults to Options.zFar.
var aspect Width over height of the viewport this was last sized for. resize computes it; assigning it alone changes nothing until the next resize.
var auto: () -> Unit Run by evaluateUniform immediately before the matrix is handed to a shader. The renderer constructor sets it to re-size to the window, so a projection built that way follows the window; one built from an explicit width and height leaves it empty and must be resized by hand.
fun resize(width: Int, height: Int): Projection
override fun evaluateUniform(): Matrix4f

SuperProjection

class SuperProjection : HasUniform<Matrix4f>

Declaration Summary
val projectionMatrix: Matrix4f The blended matrix, rebuilt in place by resize and setAspect and returned by evaluateUniform. Every dial below is read when it is rebuilt, not when it is assigned.
var fov Vertical field of view in radians, and must be positive — see Projection.fov.
var zNear Distance to the near clip plane, in view-space units; the blended matrix is renormalized so depth is 0 here. Defaults to Options.zNear.
var zFar Distance to the far clip plane, where the renormalized depth reaches 1. Defaults to Options.zFar.
var aspect Width over height of the viewport, set by resize from a window size or by setAspect directly.
var ox Sideways offset of the principal point, in half-widths of what is visible at matchDepth: 0 is centred, 1 moves the centre of the picture a half-width towards +X.
var oy The same offset vertically, in half-heights at matchDepth. Off-centre projections of this kind are what tiled renders and stereo pairs are built from.
var orthoness Where the projection sits between perspective at 0 and orthographic at 1. Values between the two are interpolated along a matrix-logarithm path rather than by blending the two matrices entry by entry, and anything outside [0,1] is clamped.
var matchDepth The view-space depth, measured along -Z, at which the perspective and orthographic ends of the blend frame exactly the same rectangle. The blend is also renormalized to hold the vertical scale there, so what the camera frames at this distance stays put as orthoness is turned and only nearer and farther geometry changes size. Zero or less falls back to the midpoint between zNear and zFar.
var autoMatchDistance: () -> Double Where auto gets matchDepth from each frame — a focus distance, say, or the camera's distance to its subject. Both constructors default it to returning matchDepth unchanged, and only the renderer constructor installs an auto that calls it.
var auto: () -> Unit Run by evaluateUniform immediately before the matrix is handed to a shader. The renderer constructor sets it to read autoMatchDistance and re-size to the window, which is what makes a projection built that way track both the window and the focus distance; one built from an explicit width and height leaves it empty.
fun resize(width: Int, height: Int): SuperProjection
fun setAspect(aspect: Float): SuperProjection
override fun evaluateUniform(): Matrix4f

VulkanProbe.kt

Reports what Vulkan this machine offers: loader, instance, devices, validation layers, the SDK and, on macOS, the KosmicKrisp driver.

VulkanProbe runs the check in a child JVM and returns a VulkanStatus, which the Vulkan panel on /welcome shows along with install steps for the current OS. VulkanDeviceInfo describes one physical device.

src/jvmMain/kotlin/afield/graphics/vulkan/util/VulkanProbe.kt — read the source

Top-level functions

Declaration Summary
fun main() Entry point for VulkanProbe.probeInChildJvm; prints one prefixed JSON line.

VulkanDeviceInfo

data class VulkanDeviceInfo(val name: String, val type: String, val apiVersion: String, val driverName: String = "", val driverInfo: String = "", val driverId: Int = 0)

One physical device as the loader reports it.

Declaration Summary
val name: String The device name the driver reports, which is what the Vulkan panel lists.
val type: String discrete, integrated, virtual, cpu or other. A machine whose only device is cpu has a software rasteriser and no GPU driver.
val apiVersion: String The highest Vulkan version this device supports, as major.minor.patch.
val driverName: String The driver's own name, from VkPhysicalDeviceDriverProperties. Empty on a driver that does not fill it in.
val driverInfo: String The driver's version string, from the same properties and equally optional.
val driverId: Int The VK_DRIVER_ID_* the driver reports, which is what tells a MoltenVK device from a KosmicKrisp one on macOS. 0 when the driver reports none. PhysicalDevice matches this number against Options.MacVulkanDriver when it picks a device.

VulkanStatus

data class VulkanStatus(val os: String, val arch: String, val ok: Boolean = false, val loaderSource: String = "none", val loaderPath: String? = null, val sdkPath: String? = null, val sdkVersion: String? = null, val instanceVersion: String? = null, val surfaceExtension: String? = null, val validationLayer: Boolean = false, val devices: List<VulkanDeviceInfo> = emptyList(), val requestedDriver: String? = null, val kosmicKrispAvailable: Boolean = false, val error: String? = null, val probeMs: Long = 0)

What a graphics box on this machine would get, and what is missing. Shown by the Vulkan panel on /welcome, where a fresh clone first lands, so the user finds out before opening a window.

Declaration Summary
val os: String macos, windows or linux.
val arch: String The JVM's os.arch. On macOS it is one of the conditions on KosmicKrisp, which runs on Apple silicon only.
val ok: Boolean A loader, an instance, and at least one device with a platform surface. False leaves error saying which of those was missing.
val loaderSource: String sdk (a LunarG SDK's loader), system (whatever the OS resolves), lwjgl-moltenvk (the MoltenVK inside LWJGL's jar, no loader), or none.
val loaderPath: String? The loader Options pinned, on macOS. Null everywhere else, where the loader is whatever the OS resolves and has no path worth printing.
val sdkPath: String? The SDK root the probe found: Options.macVulkanSdk on macOS, VULKAN_SDK elsewhere. Null when there is none, which is not by itself a failure.
val sdkVersion: String? The version read out of sdkPath's own directory name or its parent's, whichever looks like a version. Null for an SDK installed under a path that names none.
val instanceVersion: String? The highest instance API version the loader supports, as major.minor.patch. Null when the loader did not load.
val surfaceExtension: String? The platform surface extension the instance offers beside VK_KHR_surface, such as VK_EXT_metal_surface. Null means no window can be opened, and is one of the two things that clears ok.
val validationLayer: Boolean Whether VK_LAYER_KHRONOS_validation is installed. Field asks for it by default and runs without it, so false is a weaker diagnostic story rather than a broken one.
val devices: List<VulkanDeviceInfo> Every physical device the instance enumerated. Empty is the other thing that clears ok: a loader loaded, and no GPU driver offered it an ICD.
val requestedDriver: String? macOS: the driver Options asked the loader for, and whether that SDK has a KosmicKrisp to ask for.
val kosmicKrispAvailable: Boolean Whether the SDK at sdkPath ships a KosmicKrisp manifest to ask for. That it is there does not mean it is in use — requestedDriver says which one was asked for.
val error: String? What went wrong, phrased for the Vulkan panel to show, or null when nothing did. A probe that crashed loading a driver, or ran past its timeout, reports it here rather than throwing, so a failed probe still returns a status.
val probeMs: Long How long the probe took, in milliseconds. A cached status carries the duration of the probe that produced it, not of the lookup.

VulkanProbe

object VulkanProbe

Declaration Summary
fun status(refresh: Boolean = false): VulkanStatus The last probe, or a new one when there is none or refresh asks. One probe at a time.
fun probeInChildJvm(timeoutSec: Long = 30): VulkanStatus Run probeInProcess in a child JVM, because it loads every ICD the loader finds. A broken driver can crash the process that does that — Nsight already crashed it once (see Options.dumpToolingInfo) — and the process asking here is the Field server.
fun probeInProcess(): VulkanStatus Load the loader the way Field does, create a bare instance, list devices, tear it all down.

Window.kt

The GLFW window a Vulkan renderer draws into, with its input listeners and a framebuffer-to-PNG capture.

Window creates a client-API-less GLFW window (decorated, or borderless when fullscreen), tracks its size and the Renderer attached to it, and routes key, character, cursor, mouse button, scroll and focus events to listeners. Window.saveFramebufferToTempFile writes the framebuffer to a PNG; openWindowsSnapshot lists the windows currently open.

src/jvmMain/kotlin/afield/graphics/vulkan/util/Window.kt — read the source

Top-level functions

Declaration Summary
fun openWindowsSnapshot(): List<Window>

Top-level values

Declaration Summary
var lastWindowToOpen: Window? The GLFW window a Vulkan renderer draws into, with its input listeners and a framebuffer-to-PNG capture.

Window

class Window(val title : String, val fullscreen : Boolean = false, initialDimensions : Rect? = null)

Declaration Summary
val windowHandle: Long The GLFWwindow* this wraps, for GLFW calls this class does not cover. cleanup destroys it, after which it is stale.
var height: Int The framebuffer's height in pixels, which is what a swapchain or a render target is sized from. See width.
var isResized: Boolean Set when the framebuffer size changes, and when a present reports that the swapchain is out of date. The renderer reads it to decide to rebuild the swapchain and calls resetResized; reading it does not clear it.
var width: Int The framebuffer's width in pixels — twice the points on a Retina display, so it is not the frame a pointer position is measured in. size gives the window's position and size in points, and WindowPointer reports the pointer in the same points as that.
var associatedRenderer: Renderer? The renderer drawing into this window, which claims the slot when it builds its context here. A window nothing renders to leaves it null.
var presentationEnabled: Boolean Stop submitting this window while retaining its swapchain, for an optional preview.
var bestEffortPresentation: Boolean Optional mirrors skip busy swapchain images instead of pacing the other windows or OpenXR. Set before constructing the renderer.
var mouseEvents : MouseEvents? The first MouseEvents built for this window, which puts itself here and is then what OrbitControls and the stereo targets reuse rather than opening a second poller.
var keyEvents : KeyEvents? The matching slot for keyboard handlers. KeyEvents does not claim it, so unless something assigns it this stays null — construct a KeyEvents and keep it yourself.
val cursor: Vec2 Where the pointer is now, in points from the window's top-left corner, asked of GLFW on every read. The position on its own does not say that the pointer is over this window, which is what WindowPointer.inside answers.
fun size() : Rect
fun keyCallback(key: Int, scancode: Int, action: Int, mods: Int)
fun charCallback(codepoint: Int)
fun cleanup()
fun isKeyPressed(keyCode: Int): Boolean
fun addKeyListener(listener: (Int, Int, Int, Int) -> Unit): () -> Unit
fun addCharListener(listener: (Int) -> Unit): () -> Unit
fun addCursorListener(listener: (Double, Double) -> Unit): () -> Unit
fun addMouseButtonListener(listener: (Int, Int, Int) -> Unit): () -> Unit
fun addScrollListener(listener: (Double, Double) -> Unit): () -> Unit
fun addFocusListener(listener: (Boolean) -> Unit): () -> Unit
fun isMouseButtonPressed(buttonCode: Int): Boolean
fun setDimensions(rect : Rect)
fun consumeScrollDelta(): Vec2
var onPoll Per-frame work on the thread that polls this window, keyed by name. pollEvents drains GLFW's queue and then steps each sequence one step. This is how MouseEvents, KeyCamera and WindowPointer get their frame, and it is the place to put a capture or a drag that has to see every frame rather than sample when it happens to look.
fun pollEvents()
fun resetResized()
fun resize(width: Int, height: Int)
fun setShouldClose()
fun shouldClose(): Boolean
fun saveFramebufferToTempFile(imageData: ByteBuffer? = null, width: Int = this.width, height: Int = this.height): File Save the current framebuffer content to a temporary PNG file.
fun getFramebufferAsBase64(imageData: ByteBuffer? = null, width: Int = this.width, height: Int = this.height): String? Get the current framebuffer as a base64-encoded PNG string. Useful for sending image data over the network.

WindowPointer.kt

The pointer over a renderer window and the window's size, in window points, read once a frame.

src/jvmMain/kotlin/afield/graphics/vulkan/util/WindowPointer.kt — read the source

WindowPointer

class WindowPointer(val window: Window, val name: String, var onPoll: (WindowPointer) -> Unit = {})

The pointer and the window's size in points (the cursor's units, half the framebuffer's pixels on a Retina display), read on the window's poll, where GLFW allows it, and readable from any thread afterwards. A sketch that works in canvas pixels, origin top left, y down, uses width and height as its world and x, y as its mouse.

Declaration Summary
val window: Window The window whose pointer and size are read. Registering adds a sequence to its poll map, so nothing is read until the window is polled.
val name: String The key this registration takes in the window's poll map; a second pointer under the same name on the same window replaces the first.
var onPoll: (WindowPointer) -> Unit Run on every poll, after the fields below are updated, for pointer logic that has to see each frame rather than sample whenever it happens to look.
var width: Float The window's width in points; its framebuffer width until the first poll.
var height: Float The window's height in points; its framebuffer height until the first poll.
var x: Float The pointer, in points from the window's top-left corner.
var y: Float The pointer's distance down from the window's top edge, in points.
var inside: Boolean The window is under the pointer and the pointer is within it.
var down: Boolean The left button is held.
var polled: Boolean Whether the window has been polled yet. Until it has, width and height are the framebuffer's pixels, twice the points on a Retina display, so a sketch that places things by the window's size waits for this.

fmchirp.kt

FM chirp curves: an eight-parameter family of 0..1 remapping curves, packed as half floats, that evaluate the same way in Kotlin and in GLSL.

FMChirpCurves holds the parameter type, evaluation, sampling, measures such as roughness and foldover, and least-squares fitting to target points. FM_CHIRP_GLSL is the matching shader source, registered as the fmchirp shader template by registerFMChirpShaderTemplate; registerFMChirpVisualizers gives the parameter type an HTML summary when it is printed in a box.

src/jvmMain/kotlin/afield/graphics/vulkan/util/fmchirp.kt — read the source

Top-level functions

Declaration Summary
fun registerFMChirpShaderTemplate()
fun registerFMChirpVisualizers()

Top-level values

Declaration Summary
val FM_CHIRP_GLSL FM chirp curves: an eight-parameter family of 0..1 remapping curves, packed as half floats, that evaluate the same way in Kotlin and in GLSL.
val FMChirpCurves.Params.h: String
val FMChirpCurves.Params.h2: String

FMChirpCurves

object FMChirpCurves

Declaration Summary
enum class Mode
data class Range(val min: Float, val max: Float) y = x + gainx(1-x)*signedPow(q(pow(x,n)), n2)
data class FitOptions(val leastSquares: LeastSquaresOptions = LeastSquaresOptions(maxIterations = 60), val regularizationWeight: Float = 0.005f, val parameterScales: FloatArray = floatArrayOf(8f, 8f, 1f, 1f, 8f, 8f, 2f, 2f), val maxFreq: Float = 64f, val maxChirp: Float = 64f, val maxPow: Float = 8f)
data class FitStep(val modifier: CurveModifier, val before: Params, val after: Params, val result: LeastSquaresResult)
data class FitTrace(val start: Params, val result: Params, val steps: List<FitStep>)
fun interface CurveModifier
data class Params(val freq: Float = 0f, val mod: Float = 0f, val modPhase: Float = 0f, val phase: Float = 0f, val chirp: Float = 0f, val modChirp: Float = 0f, val inputPow: Float = 1f, val outputPow: Float = 1f)
fun fromVector8h(v: Vector8h): Params
fun make(freq: Float = 0f, mod: Float = 0f, modPhase: Float = 0f, phase: Float = 0f, chirp: Float = 0f, modChirp: Float = 0f, inputPow: Float = 1f, outputPow: Float = 1f): Params
fun vector8h(freq: Float = 0f, mod: Float = 0f, modPhase: Float = 0f, phase: Float = 0f, chirp: Float = 0f, modChirp: Float = 0f, inputPow: Float = 1f, outputPow: Float = 1f): Vector8h
fun fromVector(values: DoubleArray): Params
object Modifiers
object Presets
object HalfPacking
fun safePow01(x: Float, exponent: Float): Float
fun safeSignedPow(x: Float, exponent: Float): Float
fun signedPow(x: Float, exponent: Float): Float
fun exp2(x: Float): Float
fun log2(x: Float): Float
fun wrap01(x: Float): Float

superpc.kt

Push constants as a Kotlin class, and the GLSL that reads them back. A subclass of PushConstants declares its members as ordinary var properties; reflection turns those into a byte layout with explicit offsets, glsl() emits the push-constant block and accessors that match it, and writeTo serialises the current values into a buffer by those same offsets.

The reflection runs once per class and the layout is cached, so the per-frame cost is the write. Members are laid out in @PcOrder order where that annotation is given and alphabetically where it is not, which is what makes a layout stable from one run to the next.

What the generated GLSL looks like

One push-constant block, a layout(offset = …) on every member, a float register file loaded out of that block by loadRegs(), and then accessors under the names the Kotlin class used:

{{ pc.dec }}
void main() {
    loadRegs();
    uFragColor = baseColor();
}

A scalar member becomes a macro, so time in Kotlin reads as time in shader code. A vector member becomes a function of no arguments under its own name, so baseColor reads as baseColor(). To keep those macros clear of the block's own members, a member inside the block carries a leading underscore: Kotlin's time is declared as _time and the bare name is the macro that reads it out of the register file.

Instruction payloads

A property whose name already begins with an underscore is an instruction payload: it is declared in the push-constant block under exactly that name and nothing else happens to it. It is not loaded into the register file and gets no accessor, so a shader reads it from the block itself and a frame pays nothing for payloads it is not reading.

Member types

Float, Vector2f, Vector3f and Vector4f become a float, vec2, vec3 and vec4. The half-precision types travel packed into unsigned integers, to be unpacked in the shader: Vec2h as a uint, Vec3h and Vec4h as a uvec2, and the eight-lane Vector8h as a uvec4. Every member has to be a var, since the layout reads and writes through the property itself, and any other type fails when the layout is built rather than at draw time. otherDeclarations is the way around all of this: raw GLSL inserted into the block that reflection never sees, which you place yourself at nextFreeOffset(…).

class MaterialPC : PushConstants<MaterialPC>() {
    @PcOrder(0) var time: Float = 0f
    @PcOrder(1) var baseColor: Vector4f = Vector4f(1f, 1f, 1f, 1f)
    @PcOrder(2) var uvScale: Vector2f = Vector2f(1f, 1f)
    @PcOrder(100) var _patch0: Vec4h = Vector4f().half
}

val pc = MaterialPC()
pc.time = 1.0f
pc["uvScale"] = Vector2f(2f, 2f)

shaderTemplate["pc"] = mapOf("dec" to Resolver { pc.glsl() })
mesh.push.bindAll { pc }

The block reaches the shader through the template engine, as above, and the instance is bound by handing it to a mesh's push-constant structure, which writes it out each time that mesh's push constants are assembled for a draw. The buffer it is written into has to be little-endian, and a ByteBuffer is big-endian until it is told otherwise.

src/jvmMain/kotlin/afield/graphics/vulkan/util/superpc.kt — read the source

Top-level functions

Declaration Summary
fun main() Prints SmokePC's layout, its generated GLSL and a word dump of the bytes it writes.

PcOrder

annotation class PcOrder(val value: Int)

Fixes where a push-constant member sits in the block, lowest value first.

PushConstants

abstract class PushConstants<T : Any> : WritesToBuffer

Base class for a push-constant block written as Kotlin properties, which generates its own matching GLSL declaration.

Declaration Summary
open var otherDeclarations: String Optional raw GLSL declarations inserted into the push-constant block.
fun pcSizeBytes(): Int How many bytes this block occupies, rounded up to a multiple of 16.
fun regFloatCount(): Int How many floats the generated register file holds: the REG_COUNT the emitted GLSL declares.
fun nextFreeOffset(alignment: Int = 4): Int First free byte offset after all reflected fields, optionally aligned.
fun regIndexOf(name: String): Int Where name starts in the generated register file: the value of the REG_<name> constant the emitted GLSL declares.
fun pcOffsetOf(name: String): Int The byte offset of name within the push-constant block, which is the offset the generated layout(offset = …) carries.
operator fun get(name: String): Any? Dynamic getter by field name.
operator fun set(name: String, value: Any?) Dynamic setter by field name.
override fun writeTo(b: ByteBuffer, baseOffset: Int) Writes every member into b at baseOffset plus its own offset, leaving the buffer's position where it found it.
fun glsl(emitScalarLayoutQualifier: Boolean = false, regsName: String = "regs", pcInstanceName: String = "pc"): String Emits the GLSL side of this block: the push-constant declaration, a float register file, a loadRegs() that fills it, and an accessor per member under the member's own Kotlin name.

GlslGen

object GlslGen

Builds the GLSL text for a push-constant block from a layout that reflection has already worked out.

SmokePC

class SmokePC : PushConstants<SmokePC>()

A worked example of a push-constant class, covering each supported member type and both payload conventions.

Declaration Summary
var time: Float
var baseColor: Vector4f
var roughMetal: Vec2h
var uvScale: Vector2f
var emissive: Vector3f
var _patch0: Vec4h
var _patch1: Vec4h

templating.kt

The template engine behind {{ slot }} in a Field shader: a Jinja-shaped expander written against the Kotlin standard library alone, which a shader stage's source runs through before shaderc sees it. Box.shaderTemplate is where a box puts the values it resolves against.

A template may contain:

  • {{ expr }}, replaced by whatever expr resolves to.
  • {% if expr %} … {% else %} … {% endif %}, nested freely. not expr and !expr negate. An empty string, a zero, an empty map and an empty list are false; a resolver is true.
  • {% property "name" %}, which expands another named shader property in place and needs an include resolver to find it. {% include "name" %} is the same tag under its older spelling, kept so that saved documents still compile.

Everything a template reads comes from a Resolver, one function from a key to a value, and what comes back is read by value rather than by type. null means not resolved, so the next resolver in the chain is asked. A String is the answer, and so is anything else through its toString. A Resolver, a Map or a list of Resolvers is structural instead: the rest of the path is resolved against it, to a depth of fifty.

{{ a.b.c }} is matched longest prefix first, so a.b.c is offered to every resolver, then a.b, then a, then the empty key. A resolver may therefore own a whole dotted namespace or just its root, and where a answers with something structural, b.c is resolved against that. A path that resolves nowhere stops the shader rather than silently emitting nothing, and says where: the line and column, the line itself under a caret, and every key that was tried against every resolver with what each returned.

runShaderTemplate is runTemplate plus #line directives around any expansion that spans lines, so a compiler error after one still names the shader line that was written rather than the line it grew into.

src/jvmMain/kotlin/afield/graphics/vulkan/util/templating.kt — read the source

Top-level functions

Declaration Summary
fun registerGlobalShaderTemplate(name: String, template: Any) Publishes template under name for every box in the process, as the last fallback behind each box's own shaderTemplate map.
fun unregisterGlobalShaderTemplate(name: String): Any? Removes the global template registered under name and returns what was there, or null if nothing was.
fun listGlobalShaderTemplateNames(): Set<String> The names currently in the global template registry, sorted.
fun getShaderTemplateResolverForBox(b : Box): List<Resolver> The resolver chain a box's shader templates are expanded against: that box's own slots first, then the process-global registry.
fun runTemplate(template: String, resolver: Resolver, includeResolver: IncludeResolver? = null): String Expands template against a single Resolver, and returns the result.
fun runTemplate(template: String, resolver: ResolverFn, includeResolver: IncludeResolver? = null): String Expands template against a single lambda resolver, which is the same as the Resolver overload with the lambda wrapped.
fun runTemplate(template: String, resolver: Any?, includeResolver: IncludeResolver? = null): String Expands template, taking the resolver in whatever shape the caller has it.
fun runShaderTemplate(template: String, resolver: Any?, includeResolver: IncludeResolver? = null): String Expands template the way a shader stage is expanded: as runTemplate, plus #line directives around any expansion that spans more than one line.
fun main() Expands a small template against two maps and a facade resolver, and prints the result to the info,templating log channel.

Resolver

fun interface Resolver

One source of named values for a template: given a key, either a value or null for "ask the next one". A key arrives dotted and whole (a.b.c before a.b before a), so a resolver may answer for a namespace as readily as for a name, and a resolver returned as the answer resolves the rest of the path.

Declaration Summary
fun resolve(key: String): Any? Answers for key, or returns null to pass the question to the next resolver in the chain.

ResolverFn

typealias ResolverFn = (String) -> Any?

A resolver written as a plain lambda, (String) -> Any?, which the template entrypoints accept anywhere a Resolver is wanted.

IncludeResolver

fun interface IncludeResolver

Finds the source text for a {% property "name" %} tag, given the name and where the tag was written.

Declaration Summary
fun include(path: String, line: Int, col: Int): String Returns the text to splice in for the property path, which was named by a tag at line and col of the template.

MapResolver

class MapResolver(private val map: Map<String, Any?>) : Resolver

Map-backed resolver. - Looks up exact keys. - If the stored value is a Map, it returns a MapResolver to enable hierarchical resolution. - If the stored value is already a Resolver or Iterable of resolvers, returns it as-is.

Declaration Summary
override fun resolve(key: String): Any? Looks key up in the map, wrapping a nested Map as another MapResolver so the rest of a dotted path resolves against it. An absent key is null, which hands the question on.

CompositeResolver

class CompositeResolver(private val resolvers: List<Resolver>) : Resolver

A resolver that asks each of its children in turn and takes the first non-null answer.

Declaration Summary
override fun resolve(key: String): Any? Asks each child in turn and returns the first non-null answer, or null if none of them answers.

TemplateException

class TemplateException(message: String) : RuntimeException(message)

The one exception this engine throws, for every fault from an unclosed {{ to a path that no resolver answered.

util.kt

Small helpers shared by the Vulkan renderer: the host OS, memory-type selection and result checking.

updateForever builds a sequence for a mesh's update map, OS reports the platform, memoryTypeFromProperties picks a device memory type, and vkCheck throws on a failed Vulkan call.

src/jvmMain/kotlin/afield/graphics/vulkan/util/util.kt — read the source

Top-level functions

Declaration Summary
fun updateForever(function: () -> Boolean) A Sequence that calls function once per step, for mesh.update["name"] = updateForever { ... }.
fun memoryTypeFromProperties(physDevice: PhysicalDevice, _typeBits: Int, reqsMask: Int): Int Picks a device memory type index for an allocation, given the types Vulkan says are allowed for it and the properties it needs.
fun vkCheck(err: Int, errMsg: String) Throws unless err is VK_SUCCESS, which is how every Vulkan call in the renderer is checked.

Top-level values

Declaration Summary
val OS: OSType Which of the four platforms Field recognises this JVM is running on, read from the os.name system property.

OSType

enum class OSType

The host platforms the renderer distinguishes, as reported by OS.

Constants: WINDOWS, MACOS, LINUX, OTHER.

Syphon.kt

Field's end of Syphon, the macOS standard for sharing GPU frames between applications: a SyphonServer publishes frames that Resolume, TouchDesigner, MadMapper, OBS (with its Syphon source), VDMX or Max can receive, and a SyphonClient receives what they publish.

val out = syphonServer("Field")             // appears in every Syphon client's list
out.publish(image)                          // a BufferedImage, RGBA bytes, or a RenderToTexture

val inp = syphonClient("Resolume Arena")    // a server's name, app name or uuid
display("main", inp.png())                  // the latest frame

Field reaches Syphon through a small Objective-C library, native/field-syphon, that is optional and macOS-only — build it once with ./gradlew buildSyphonNative (needs Xcode).

Every frame crosses the boundary through memory today: a RenderToTexture is read back from the GPU and copied into Syphon's shared IOSurface, and a received frame is read back into a ByteArray. At 1920 x 1080 that is about 8 MB a frame each way. Publishing an IOSurface that already exists (SyphonServer.publishIOSurface) skips the CPU entirely.

src/jvmMain/kotlin/afield/io/syphon/Syphon.kt — read the source

Used in: Syphon: sharing frames on macOS

Top-level functions

Declaration Summary
fun syphonServer(name: String = "Field"): SyphonServer The open Syphon server called name, or a new one. Other applications list it under Field's app name and this name.
fun syphonClient(match: String = "", waitMs: Int = 2000): SyphonClient Connect to the first server whose uuid, name, or app name (tried in that order) is match; an empty match takes the first server announced. Waits up to waitMs for it to appear, then throws, listing the servers it could see. Reuses the open client cached under exactly match; a missing or closed client is recreated. Calls with different matches have separate cache slots. Clients persist across box runs; close one explicitly when its shared connection is no longer needed.

Syphon

object Syphon

Process-wide Syphon state: whether the bridge is built, the server list, and the run-loop pump.

Declaration Summary
var mainLoopTurns: Long Run-loop turns given to Syphon on the process's main thread (see ensurePump).
val cpuCopies: Long Frames this JVM has read back through memory from any Syphon client (latest(), png(), and a SyphonTexture on its copy path). A zero-copy SyphonTexture leaves it unchanged.
var offMainCalls: Long Pump calls that found themselves off the main thread, and so did nothing.
val isAvailable: Boolean Whether the native bridge is built (and this is a Mac).
fun describe(): String A sentence saying where the bridge is, or how to build it.
fun servers(waitMs: Int = 500): List<SyphonServerInfo> Every Syphon server announced on this machine — Field's own included. The first call waits up to waitMs for announcements to arrive.

SyphonServerInfo

data class SyphonServerInfo(val name: String, val appName: String, val uuid: String)

One announced server: what SyphonClient can connect to.

Declaration Summary
override fun toString(): String

SyphonServer

class SyphonServer internal constructor(val name: String) : AutoCloseable

A Syphon server: frames published here are visible to every Syphon client on the machine.

Declaration Summary
var framesPublished: Long Frames published so far.
val info: SyphonServerInfo This server as other applications see it.
val hasClients: Boolean Whether any application is receiving.
fun publish(pixels: ByteArray, width: Int, height: Int, bgra: Boolean = false) Publish width x height pixels, four bytes each, top row first: RGBA order, or BGRA when bgra is true (what a B8G8R8A8 render target reads back as). The bytes are copied; reuse the array at once.
fun publish(argb: IntArray, width: Int, height: Int) Publish ARGB ints (as BufferedImage.getRGB and afield.media.image.util.PngEncoder use), top row first.
fun publish(image: BufferedImage) Publish a BufferedImage (any type; it is converted to ARGB).
fun publish(target: RenderToTexture) Publish the next frame target renders: its colour attachment is read back once that frame's GPU work completes, then handed to Syphon. Returns at once. The target needs 8-bit RGBA or BGRA colour (the default B8G8R8A8_SRGB is fine) and colorTransferSrc = true (the default).
fun follow(target: RenderToTexture) Publish every frame target renders, until stopFollowing, close, or another call to follow. One readback is in flight at a time, so a slow consumer drops frames instead of queueing them.
fun stopFollowing() Stop following and followOnGpu.
fun followOnGpu(target: RenderToTexture, renderer: Renderer = fromContext()): SyphonGpuPublisher Publish every frame target renders without a CPU readback: each frame is copied on the GPU into an IOSurface and Syphon takes it from there. Needs a B8G8R8A8 target (the default) and a driver with VK_EXT_external_memory_host (KosmicKrisp and MoltenVK on Apple silicon); call it after target is in the renderer's list, since it renders after whatever is already there. Replaces any earlier follow or followOnGpu; stopFollowing ends it.
fun publishIOSurface(ioSurfaceRef: Long, flipped: Boolean = false) Publish an existing BGRA8 IOSurface by its IOSurfaceRef address, without touching the CPU: Syphon copies it GPU-side into its own shared surface. flipped says the surface's first row is the bottom of the picture.
override fun close() Stop publishing; clients see the server disappear.
override fun toString(): String

SyphonClient

class SyphonClient internal constructor(private val handle: MemorySegment) : AutoCloseable

A connection to another application's Syphon server. latest returns its most recent frame. Holds native resources until close.

Declaration Summary
val isClosed: Boolean Whether this client's native connection has been released.
val server: SyphonServerInfo The server this client is connected to.
val frameCount: Long Frames the server has sent since this client connected.
fun waitForFrame(after: Long = frameCount, waitMs: Int = 1000): Long Wait up to waitMs for a frame after the afterth; returns the frame count then.
fun latest(): SyphonFrame? The latest frame, or null if none has arrived yet.
fun texture(renderer: Renderer = fromContext(), waitMs: Int = 2000, zeroCopy: Boolean = true): SyphonTexture This client's frames as a Vulkan texture that follows the server: bind the returned SyphonTexture as a sampler2D like any other texture. Where the Vulkan driver allows it the shader samples the sender's IOSurface itself, with nothing copied (SyphonTexture.path); zeroCopy = false asks for the old route through memory. Waits up to waitMs for a first frame. Registered in renderer's list, which is what refreshes it each frame.
fun png(): ByteArray? The latest frame as a PNG (for display), or null if none has arrived yet.
override fun close()
override fun toString(): String

SyphonFrame

class SyphonFrame(val rgba: ByteArray, val width: Int, val height: Int)

One received frame: rgba is width x height x 4 bytes, top row first.

Declaration Summary
fun rgbaAt(x: Int, y: Int): IntArray The RGBA of pixel (x, y), y down from the top, as 0..255 ints.
fun argb(): IntArray ARGB ints, one per pixel, top row first.
fun image(): BufferedImage
fun png(): ByteArray
override fun toString(): String

SyphonGpu.kt

Publishes every frame a RenderToTexture draws to a SyphonServer without the CPU touching the pixels: the frame is copied GPU-side into an IOSurface (on the same command buffer that drew it), and once that frame's GPU work has completed Syphon blits the IOSurface into its own shared one.

Made by SyphonServer.followOnGpu; a Target in the renderer's list, placed after the source so it runs after the source has drawn. The IOSurface reaches Vulkan as host memory (VK_EXT_external_memory_host, see IOSurfaceHostBuffer), which both KosmicKrisp and MoltenVK offer on Apple silicon. Two surfaces alternate; if Syphon is still copying the one due next, the frame is skipped rather than overwritten under it.

src/jvmMain/kotlin/afield/io/syphon/SyphonGpu.kt — read the source

SyphonGpuPublisher

class SyphonGpuPublisher internal constructor(val server: SyphonServer, val source: RenderToTexture, private val renderer: Renderer) : Target, afield.graphics.vulkan.targets.RendersAfter, AutoCloseable

A Target that copies source's frame into an IOSurface and hands it to server each frame.

Declaration Summary
val server: SyphonServer
val source: RenderToTexture
override val rendersAfter: Target Renders after source whatever order the two were added in.
var published: Long Frames handed to Syphon, and frames skipped because the next surface was still in use.
var skipped: Long
override fun render(command: CommandBuffer, index: Int)
override fun close() Stop publishing. The surfaces are freed once the GPU and Syphon are done with them, which is at most a frame or two later.
override fun toString(): String

SyphonTexture.kt

A SyphonClient's frames as a texture for a shader to sample: bind this object itself as a sampler2D (prop<SyphonTexture>(name) = client.texture()). Made by SyphonClient.texture, and kept current by the renderer's list, which calls render each frame.

Syphon's frame is an IOSurface that the sending process draws into. Where the driver allows it, Vulkan samples that IOSurface's own memory, with no copy of any kind (path says which route):

  • Path.IMPORTED_IMAGE: MoltenVK. The IOSurface backs a VkImage (VK_EXT_metal_objects).
  • Path.HOST_LINEAR_IMAGE: KosmicKrisp (and MoltenVK if the first is refused). The IOSurface's pages are imported as host memory (VK_EXT_external_memory_host) and bound to a linearly tiled VkImage whose row pitch is the surface's own.
  • Path.HOST_BUFFER_COPY: the pages are imported as a buffer and copied into an image on the GPU, once per new frame; used when the driver will not lay a linear image over them.
  • Path.CPU_COPY: the fallback: a blit into a staging buffer, a swizzle on the CPU, and an upload. Also what texture(zeroCopy = false) asks for.

The texture is B8G8R8A8_SRGB on the zero-copy paths and R8G8B8A8_SRGB on the copy path; either way the shader sees RGBA, top row first (v = 0 is the top of the sender's picture). When the sender changes size Syphon replaces the IOSurface, and the texture re-imports the new one; the old one is released only after the frame that last sampled it has completed. (The copy path keeps the first frame's size and skips frames of another.) Like every Syphon client, the zero-copy paths read the sender's surface while it may be drawing the next frame into it; Syphon has no cross-process fence to wait on.

src/jvmMain/kotlin/afield/io/syphon/SyphonTexture.kt — read the source

SyphonTexture

class SyphonTexture internal constructor(val client: SyphonClient, private val renderer: Renderer, zeroCopy: Boolean = true) : Target, HasImageView, HasImageViewArray, PreparesSampledImages, HasDefaultTextureSampler, AutoCloseable

A sampled texture that tracks a SyphonClient's latest frame; see the file overview for its paths.

Declaration Summary
val client: SyphonClient
enum class Path
var path: Path How the current frame reaches the shader.
var fallbackReason: String Why the zero-copy routes were not taken, when path is not one of them; else empty.
var frames: Long New frames seen, frames that went through memory (CPU copies), and IOSurface (re-)imports.
var cpuCopies: Long
var imports: Long
var recordNanos: Long Nanoseconds this object spent on the render thread handling new frames, summed over frames: on the copy path the blit, wait, swizzle and staging write; on the zero-copy paths asking Syphon for the surface and comparing its id.
var skipped: Long Frames skipped on the copy path because their size changed.
val width: Int
val height: Int
override var defaultSampler: TextureSampler?
override val imageView: ImageView
override val imageViewArray: List<HasImageView>
override fun render(command: CommandBuffer, index: Int)
override fun prepareSampledImagesFor(command: CommandBuffer)
override fun close()
override fun toString(): String

SolidAgreement.kt

The measurement this package exists for: one written shape, evaluated two completely independent ways, and the disagreement reported as a number.

val bracket = cuboid(4, 3, 1) - cylinder(2, 0.5).at(1, 0, 0)
println(bracket.crossCheck(SolidQuality(edgeLength = 0.02, segments = 256)).report())

Two implementations that share no code

Almost nothing in a graphics codebase gets to do this. The usual options are to assert against a remembered constant (which pins the current behaviour, not the correct one) or against a closed form (which only exists for a handful of shapes). Here both sides are implementations, and they share no code whatsoever:

side A side B
the shape becomes a distance field over Doubles a CSG tree over Manifold handles
the surface comes from afield.geometry.mesh.marchingCubes, in Kotlin Manifold's exact boolean kernel, in C++
the volume comes from a sum of signed tetrahedra over Float triangles Manifold's own integration, in double
the genus comes from edge/vertex adjacency in mesh_topology.kt Manifold's Euler characteristic

There is no path by which a bug in one produces the same wrong answer in the other. Five-figure agreement is therefore strong evidence that both are right, in a way that any number of assertions against constants would not be.

The same argument covers a third reading, the differentiable one in SolidGradient.kt, which is toScalarField's arithmetic replaced by afield.math.autodiff.AD and shares no line of code with it. It is measured by SolidExpr.gradientAgreement rather than folded in here, and the reason is that the two compare different objects: this file compares surfaces — volume, area, genus, Hausdorff deviation — and the differentiable reading produces a field, exactly as toScalarField does. Folding it in would have meant inventing a volume for something that has none. What the two share is the argument, so each cross-references the other.

Agreement does not converge to zero

There are two regimes:

  • Polyhedral shapes — cuboids, polygon-profile extrusions, and booleans of those. The exact lowering is the truth, so the marched one converges to it and the residual goes to zero as SolidQuality.edgeLength shrinks. Expect the relative volume error to fall roughly as h².
  • Curved shapes — anything with a sphere, a cylinder, a circular profile or a round offset. Manifold's circle is an inscribed n-gon, whose area falls short of the true one by 1 − (n/2π)·sin(2π/n) ≈ 2π²/3n². The field lowering describes the true curve. So agreement converges to that deficit and not to zero, however fine the march.

facetingDeficit computes the second one from the segment count alone, so a curved shape's residual can be checked against a predicted floor rather than a remembered one. That is the difference between "these two numbers are close" and "these two numbers are as close as the geometry permits, and here is the formula".

src/jvmMain/kotlin/afield/lang/solidsl/SolidAgreement.kt — read the source

Top-level functions

Declaration Summary
fun SolidExpr.crossCheck(quality: SolidQuality = SolidQuality()): SolidAgreement Build this shape both ways and measure the disagreement.
fun facetingDeficit(n: Int): Double The relative volume a regular inscribed n-gon loses against its circle: 1 − (n / 2π)·sin(2π / n), which is 2π²/3n² to leading order.
fun effectiveSegments(radius: Double, requested: Int): Int How many segments Manifold will actually give a circle of this radius under the current global quality, when a node asked for 0.
fun SolidExpr.marchedMesh(quality: SolidQuality = SolidQuality()): TriMesh TriMesh from the marched side of a cross-check, for a render or a second opinion.

AgreementKind

enum class AgreementKind

Which two things were compared — see the note on SolidAgreement about the empty bucket.

Constants: EXACT_VS_MARCHED, MARCHED_VS_MARCHED.

SolidAgreement

data class SolidAgreement(val expr: SolidExpr, val kind: AgreementKind, val edgeLength: Double, val segments: Int, val refusals: List<String>, val solidVolume: Double, val marchedVolume: Double, val solidArea: Double, val marchedArea: Double, val solidGenus: Int, val marchedGenus: Int?, val marchedIsWatertight: Boolean, val solidTriangles: Long, val marchedTriangles: Int, val deviation: Double, val scale: Double)

What the two lowerings of one SolidExpr said, and how far apart they were.

Declaration Summary
val expr: SolidExpr
val kind: AgreementKind
val edgeLength: Double
val segments: Int
val refusals: List<String>
val solidVolume: Double Manifold's own integration of the exact (or level-set) solid.
val marchedVolume: Double afield.geometry.mesh.signedVolume over the marched triangles — a sum of signed tetrahedra.
val solidArea: Double
val marchedArea: Double
val solidGenus: Int
val marchedGenus: Int? null when the marched mesh came out open, which is itself a failure worth seeing.
val marchedIsWatertight: Boolean
val solidTriangles: Long
val marchedTriangles: Int
val deviation: Double Symmetric vertex-sampled Hausdorff distance between the two surfaces, in model units.
val scale: Double The longest axis of the shape's bound — what deviation is relative to.
val relativeVolumeError: Double \|Va − Vb\| / max(\|Va\|, \|Vb\|).
val relativeAreaError: Double \|Aa − Ab\| / max(\|Aa\|, \|Ab\|).
val relativeDeviation: Double deviation as a fraction of the shape's longest axis, so it compares across sizes.
val genusAgrees: Boolean true when both sides closed and agreed on how many holes there are.
fun line(): String One line, for a sweep printed as a table.
fun report(): String The whole story, for a println in a box.

SolidExact.kt

Lowering (b): a SolidExpr as an exact Manifold Solid — or a stated refusal.

when (val l = (cuboid(2, 2, 1) - cylinder(3, 0.3)).exactSolid()) {
    is SolidLowering.Lowered -> println("volume ${l.value.volume}, genus ${l.value.genus}")
    is SolidLowering.Refused -> println("no exact form: ${l.why}")
}

Refusals are in the type

The discipline is afield.lang.rectsl.RectSLFnLowering's, wholesale, and for the same reason it gives: a wrong answer is worse than no answer, so the lowering never guesses. Every node either maps to the corresponding Manifold call or refuses with a reason, and the refusal propagates up.

A null would invite a caller to substitute something plausible; a thrown exception loses the reason at the first runCatching. A stated reason survives into a test assertion, a diagnostic and a docs page — which is why the refusals in this package are enumerable and enumerated:

node why there is no exact form flavour
SolidExpr.SmoothUnion the fillet surface lies on neither operand, so no sequence of unions, intersections and differences of the two can produce it permanent
SolidExpr.SmoothDifference the same, for the filleted cut permanent
SolidExpr.Gyroid a trigonometric field on all of space, not a bounded volume permanent
SolidExpr.Round minkowskiSum with a sphere exists, but that sphere is an inscribed polyhedron, so the result is strictly inside the true offset by up to its sagitta — and refining the march does not close a gap that is not the march's would not converge
SolidExpr.Shell grow − erode exists as two Minkowski operations, with SolidExpr.Round's problem twice over and a concave decomposition between them would not converge

The last two are the interesting ones. They are not "we did not get to it": an exact-looking answer is available and is the wrong answer, systematically and in a direction that does not shrink. Filing them beside the smooth blends would flatten a real distinction, so the type carries SolidLowering.Refused.closable and the reason says which kind it is.

Note what is not on the list. Every Profile node lowers exactly — including Profile.Offset, because Section.offset(d, JoinType.ROUND) is definitionally the Minkowski sum with a disc, which is exactly what sdf − d means. So a rounded slot has an exact form and a rounded solid does not, and the asymmetry is real: Clipper offsets a 2D region as a region, where the 3D story has to go through a polyhedral ball.

src/jvmMain/kotlin/afield/lang/solidsl/SolidExact.kt — read the source

Used in: A solid, written down: afield.lang.solidsl

Top-level functions

Declaration Summary
fun <T> SolidLowering<T>.valueOrNull(): T? The lowered value, or null for a refusal.
fun SolidLowering<*>.refusalOrNull(): String? The refusal reason, or null when this did lower.
fun <T> SolidLowering<T>.orThrow(): T The lowered value, or a thrown IllegalStateException carrying the refusal.
fun SolidExpr.exactSolid(quality: SolidQuality = SolidQuality()): SolidLowering<Solid> The exact Manifold Solid for this shape, or the refusal that explains why there is not one.
fun Profile.exactSection(quality: SolidQuality = SolidQuality()): Section The exact Manifold Section for this region. Every Profile node has one, so this never refuses.
fun SolidExpr.exactRefusal(): SolidLowering.Refused? The refusal this shape's exact lowering would produce, or null if it has an exact form — answered from the expression, with no native call and nothing built.
fun SolidExpr.children(): List<SolidExpr> This node's immediate SolidExpr children, in written order. The one place the shape of the tree is listed.
fun SolidExpr.allRefusals(): List<SolidLowering.Refused> Every refusal in this expression, not just the first — the catalogue a docs page or a report wants.
fun SolidExpr.buildSolid(quality: SolidQuality = SolidQuality()): SolidBuild A Solid built from this expression, taking the exact path everywhere it exists and the level set only where it does not — at the smallest subtree that needs it.

SolidLowering

sealed interface SolidLowering<out T>

Either a lowering into Manifold's world, or a refusal that says why.

Declaration Summary
data class Lowered<out T>(val value: T) : SolidLowering<T> The lowering exists: value is the Solid or Section.
data class Refused(val why: String, val node: String = "", val closable: Boolean = false, val missing: String = "") : SolidLowering<Nothing> There is no faithful exact form, why is the reason in one sentence, and closable says whether that is a gap in ambition or a gap in mathematics.

SolidQuality

data class SolidQuality(val segments: Int = 0, val edgeLength: Double? = null, val edgeLengthFraction: Double = 0.01, val levelSetTolerance: Double = -1.0)

How finely the two evaluations are asked to work.

Declaration Summary
val segments: Int
val edgeLength: Double?
val edgeLengthFraction: Double
val levelSetTolerance: Double
fun edgeLengthFor(bounds: AxisAlignedBoundingBox3D): Double The edge length to march bounds at: edgeLength if it was given, else a fraction of the longest axis.

SolidBuild

class SolidBuild internal constructor(val solid: Solid, val marchedSubtrees: List<MarchedSubtree>) : AutoCloseable

What buildSolid produced, and how much of it was exact.

Declaration Summary
val solid: Solid The solid. The caller owns it — close() it, or build inside a Solid.scope.
val marchedSubtrees: List<MarchedSubtree> Every subtree that had to be marched, with the reason it could not be built exactly.
val isExact: Boolean true when nothing had to be marched.
fun report(): String A one-paragraph account of what happened, for a println in a box.
override fun close()

MarchedSubtree

data class MarchedSubtree(val node: SolidExpr, val why: String, val edgeLength: Double, val bounds: AxisAlignedBoundingBox3D)

One subtree that the exact lowering refused and the level set took instead.

Declaration Summary
val node: SolidExpr
val why: String
val edgeLength: Double
val bounds: AxisAlignedBoundingBox3D

SolidExpr.kt

The SolidSL algebra: 3D solids and 2D profiles as immutable data values, with the builder functions that write them and the bounds that enclose them.

A SolidExpr is a tree of primitives (sphere, cuboid, cylinder, cone, torus, gyroid), transforms (at, turned, sized, mirrored), booleans (+, -, unionOf, differenceOf, intersectionOf, smoothUnion, smoothDifference) and modifiers (rounded, shelled). A Profile is the 2D counterpart, turned into a solid by extruded or revolved.

val bracket = cuboid(60, 40, 8) - Profile.rect(26, 9).offset(4.5).extruded(30).at(0, 0, -15)
val field = bracket.toScalarField()
val exact = bracket.exactSolid()

This file only builds and measures the tree. Reading it is done elsewhere in the package: toScalarField lowers it to a sampled distance field, exactSolid to Manifold's exact CSG, toGlsl to a shader function, and buildSolid mixes the first two.

src/jvmMain/kotlin/afield/lang/solidsl/SolidExpr.kt — read the source

Used in: A solid, written down: afield.lang.solidsl, Solids that cannot fail: Manifold in Field

Top-level functions

Declaration Summary
fun sphere(radius: Number = 1.0, segments: Int = 0): SolidExpr A sphere of radius centred on the origin. See SolidExpr.Sphere for what segments costs.
fun cuboid(x: Number, y: Number, z: Number): SolidExpr An axis-aligned box of full extents x by y by z, centred on the origin.
fun cuboid(size: Number): SolidExpr A cube of side size, centred on the origin.
fun cylinder(height: Number, radius: Number, radiusTop: Number = radius, segments: Int = 0): SolidExpr A cylinder along Z, centred on the origin. Pass a different radiusTop for a frustum.
fun cone(height: Number, radius: Number, segments: Int = 0): SolidExpr A cone along Z, centred on the origin, point at +height/2.
fun torus(major: Number, minor: Number, segments: Int = 0): SolidExpr A torus: the ring of tube radius minor at distance major from the Z axis, lying in the XY plane.
fun gyroid(scale: Number = 3.0, thickness: Number = 0.2): SolidExpr The gyroid sheet — see SolidExpr.Gyroid, and note that it is unbounded.
operator fun SolidExpr.plus(other: SolidExpr): SolidExpr Union — Solid.union in the exact lowering, pointwise min in the field one.
operator fun SolidExpr.minus(other: SolidExpr): SolidExpr Difference — this shape with other removed.
infix fun SolidExpr.and(other: SolidExpr): SolidExpr Intersection.
fun SolidExpr.at(x: Number, y: Number, z: Number): SolidExpr Moved by (x, y, z).
fun SolidExpr.at(offset: Vec3): SolidExpr Moved by offset.
fun SolidExpr.turned(x: Number = 0.0, y: Number = 0.0, z: Number = 0.0): SolidExpr Rotated by Euler angles in degrees, X then Y then Z.
fun SolidExpr.sized(factor: Number): SolidExpr Uniformly scaled about the origin.
fun SolidExpr.sized(x: Number, y: Number, z: Number): SolidExpr Scaled per axis about the origin — see SolidExpr.Scale for what a non-uniform one costs.
fun SolidExpr.mirrored(x: Number, y: Number, z: Number): SolidExpr Mirrored through the plane with this normal, through the origin.
fun SolidExpr.rounded(radius: Number): SolidExpr Grown outward by radius, rounding every convex edge. See SolidExpr.Round — the exact path refuses.
fun SolidExpr.shelled(thickness: Number): SolidExpr Hollowed to a wall of 2 * thickness. See SolidExpr.Shell — the exact path refuses.
fun smoothUnion(a: SolidExpr, b: SolidExpr, k: Number = 0.2): SolidExpr A filleted union of radius roughly k. See SolidExpr.SmoothUnion — the exact path refuses.
fun smoothDifference(a: SolidExpr, b: SolidExpr, k: Number = 0.2): SolidExpr A filleted difference. See SolidExpr.SmoothDifference — the exact path refuses.
fun unionOf(children: List<SolidExpr>): SolidExpr SolidExpr.Union of a list — the batch form, which is what Manifold wants.
fun intersectionOf(children: List<SolidExpr>): SolidExpr SolidExpr.Intersect of a list.
fun differenceOf(children: List<SolidExpr>): SolidExpr SolidExpr.Difference of a list: the first minus all the rest.
operator fun Profile.plus(other: Profile): Profile
operator fun Profile.minus(other: Profile): Profile
infix fun Profile.and(other: Profile): Profile
fun Profile.offset(delta: Number, segments: Int = 0): Profile Grown by delta with a round join — see the note on Profile. Negative shrinks.
fun Profile.translated(x: Number, y: Number): Profile
fun Profile.turned(degrees: Number): Profile Rotated about the origin, in degrees, counter-clockwise.
fun Profile.sized(factor: Number): Profile
fun Profile.sized(x: Number, y: Number): Profile
fun Profile.extruded(height: Number, twistDegrees: Number = 0.0, scaleTop: Number = 1.0, slices: Int = 0): SolidExpr Swept along Z from z = 0 to z = height. See SolidExpr.Extrude.
fun Profile.revolved(degrees: Number = 360.0, segments: Int = 0): SolidExpr Revolved about Z (the profile's y becomes z); the profile must lie at x >= 0. See SolidExpr.Revolve.
fun SolidExpr.bounds(): AxisAlignedBoundingBox3D? A conservative axis-aligned bound on this shape, computed from the expression with no sampling — or null when the shape is unbounded (SolidExpr.Gyroid, or anything unioned with one).
fun Profile.bounds(): Rect2? A conservative bound on this 2D region, or null if it is unbounded.
fun AxisAlignedBoundingBox3D.expandedBy(m: Double): AxisAlignedBoundingBox3D These bounds grown outward by m on every axis.

SolidExpr

sealed interface SolidExpr

A 3D solid written down as a value instead of as a closure — the symbolic notation that afield.geometry.mesh's signed-distance family and afield.geometry.solid.manifold's exact CSG kernel can both evaluate.

Declaration Summary
data class Sphere(val radius: Double, val segments: Int = 0) : SolidExpr A sphere of radius centred on the origin.
data class Cuboid(val x: Double, val y: Double, val z: Double) : SolidExpr An axis-aligned box of full extents x by y by z, centred on the origin.
data class Cylinder(val height: Double, val radiusLow: Double, val radiusHigh: Double = radiusLow, val segments: Int = 0) : SolidExpr A cylinder, cone or truncated cone along Z, centred on the origin, so z runs from -height/2 to +height/2.
data class Extrude(val profile: Profile, val height: Double, val twistDegrees: Double = 0.0, val scaleTop: Double = 1.0, val slices: Int = 0) : SolidExpr A Profile swept along Z from z = 0 to z = height, optionally twisting and tapering.
data class Revolve(val profile: Profile, val degrees: Double = 360.0, val segments: Int = 0) : SolidExpr A Profile revolved about Z: the profile's x becomes the radius in the XY plane and its y becomes z. The profile must lie at x >= 0; anything at negative x is discarded rather than folded, which is Manifold's behaviour and is therefore this notation's.
data class Gyroid(val scale: Double = 3.0, val thickness: Double = 0.2) : SolidExpr The gyroid, sin x cos y + sin y cos z + sin z cos x, thresholded to a sheet of thickness.
data object Empty : SolidExpr Nothing. The unit of Union and the annihilator of Intersect.
data class Translate(val child: SolidExpr, val x: Double, val y: Double, val z: Double) : SolidExpr
data class Rotate(val child: SolidExpr, val x: Double, val y: Double, val z: Double) : SolidExpr Rotation by Euler angles in degrees, applied X then Y then Z — Manifold's own order.
data class Scale(val child: SolidExpr, val x: Double, val y: Double, val z: Double) : SolidExpr Scale per axis about the origin.
data class Mirror(val child: SolidExpr, val x: Double, val y: Double, val z: Double) : SolidExpr Mirror through the plane with this normal, through the origin.
data class Round(val child: SolidExpr, val radius: Double) : SolidExpr This shape grown outward by radius, rounding every convex edge — f(p) - r.
data class Shell(val child: SolidExpr, val thickness: Double) : SolidExpr A hollow shell of wall thickness 2 * thickness about this shape's surface — \|f(p)\| - t.
data class Union(val children: List<SolidExpr>) : SolidExpr Everything inside any child.
data class Intersect(val children: List<SolidExpr>) : SolidExpr Everything inside every child.
data class Difference(val children: List<SolidExpr>) : SolidExpr The first child with all the others removed.
data class SmoothUnion(val a: SolidExpr, val b: SolidExpr, val k: Double = 0.2) : SolidExpr A union with a fillet of radius roughly k where the surfaces meet — the polynomial smooth minimum, the operator that makes field modelling feel like clay.
data class SmoothDifference(val a: SolidExpr, val b: SolidExpr, val k: Double = 0.2) : SolidExpr SmoothUnion's counterpart for subtraction — a drilled hole whose rim is not a knife edge.

Profile

sealed interface Profile

A 2D region, written down the same way — the thing SolidExpr.Extrude sweeps and SolidExpr.Revolve turns.

Declaration Summary
data class Circle(val radius: Double, val segments: Int = 0) : Profile A disc of radius centred on the origin.
data class Rect(val x: Double, val y: Double) : Profile An axis-aligned rectangle of full extents x by y, centred on the origin.
data class Polygon(val points: List<P2>) : Profile A closed polygon through points, counter-clockwise for a positive region.
data class Offset(val child: Profile, val delta: Double, val segments: Int = 0) : Profile This region grown by delta with a round join — see the note on Profile.
data class Translate(val child: Profile, val x: Double, val y: Double) : Profile
data class Rotate(val child: Profile, val degrees: Double) : Profile Rotation about the origin, in degrees, counter-clockwise.
data class Scale(val child: Profile, val x: Double, val y: Double) : Profile
data class Union(val children: List<Profile>) : Profile
data class Intersect(val children: List<Profile>) : Profile
data class Difference(val children: List<Profile>) : Profile
data object Empty : Profile

P2

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

An immutable 2D point, in Double.

Declaration Summary
fun toVec2(): Vec2

Rect2

data class Rect2(val minX: Double, val minY: Double, val maxX: Double, val maxY: Double)

A 2D bounding rectangle, in Double — Profile's counterpart of AxisAlignedBoundingBox3D.

Declaration Summary
fun union(other: Rect2)
fun intersect(other: Rect2)
fun expandedBy(m: Double)
fun radius(): Double The distance from the origin to the farthest corner — the radius a twist sweeps through.

SolidField.kt

Lowering (a): a SolidExpr as a signed distance field, so everything mesh_sdf.kt, marchingCubes and Solid.levelSetOf already do keeps working unchanged.

val mesh = (cuboid(2, 2, 1) - cylinder(3, 0.3)).march(resolution = 96)

This lowering always succeeds

Unlike exactSolid, this lowering always succeeds. Every node in the algebra has a field form — that is where the algebra came from. What it does not always have is an exact distance function, and the difference matters to a ray marcher (which steps by the field's value and overshoots if the value is an overestimate) even though it does not matter to a marching cubes (which only reads signs). fieldNotes enumerates the places where the field is sign-correct but not metric-correct, so the gap is a fact rather than a surprise.

It is thread-safe, where the sdf… family is not

mesh_sdf.kt is explicit that a ScalarField3D "must read the vector it is handed and not write to it", and its own combinators capture a single scratch Vec3 to transform into. That makes them fast and single-threaded.

This lowering does not do that. Internally it builds a tree of Field3 — a fun interface over three primitive Doubles, so there is no boxing and nothing to alias — and only wraps a Vec3 at the very top, in toScalarField. So the field is re-entrant and safe to call from any thread.

src/jvmMain/kotlin/afield/lang/solidsl/SolidField.kt — read the source

Used in: A solid, written down: afield.lang.solidsl

Top-level functions

Declaration Summary
fun SolidExpr.toScalarField(): ScalarField3D This shape as a ScalarField3D, negative inside — the convention sdfSphere and friends use, so this composes straight into sdfUnion, marchingCubes and Solid.levelSetOf.
fun SolidExpr.toLevelSetField(): Field3 This shape's field in the sign convention Manifold's levelSet wants — positive inside — and over primitive doubles, so no Vec3 is ever constructed.
fun SolidExpr.march(resolution: Int = 64, bounds: AxisAlignedBoundingBox3D? = null, margin: Double = 0.0): TriMesh marchingCubes on toScalarField, with the shape's own bounds as the window.
fun SolidExpr.fieldNotes(): List<String> Every place in this expression where the field lowering is sign-correct but not a true distance function, in the order they appear.
fun SolidExpr.toField3(): Field3 This shape as a Field3, negative inside. The compiled form both other entry points share.
fun Profile.toField2(): Field2 This region as a Field2, negative inside.

Field3

fun interface Field3

A scalar field over three primitive doubles.

Declaration Summary
fun at(x: Double, y: Double, z: Double): Double

Field2

fun interface Field2

A scalar field over two primitive doubles — Field3's 2D counterpart, for Profile.

Declaration Summary
fun at(x: Double, y: Double): Double

SolidGlsl.kt

Lowering (c): the same SolidExpr as GLSL, so the shape that produces the fabricable mesh can also be ray-marched on a GPU at sixty frames a second.

val part = cuboid(2, 1.2, 0.4) - cylinder(2, 0.3).at(0.5, 0, 0)
println(part.toGlsl().glsl)          // float sdSolid(vec3 p) { … }
println(part.toRaymarchFragment())   // a complete fragment shader that draws it

This is the lowering that only a symbolic notation can have. A (Vec3) -> Float runs on the JVM and nowhere else; an expression tree can be walked into whatever language you like. RectSL does the same in 2D, in RectSLGlsl.emitSdf.

What is emitted, and the unusual shape of it

Straight-line, single-assignment, entirely scalar GLSL: no vec2, no vec3 locals, no swizzles, no loops, no branches except the conditional operator.

float sdSolid(vec3 p) {
    float x0 = p.x;
    float y0 = p.y;
    float z0 = p.z;
    float q1 = abs(x0) - 1.0;
    …
    return d7;
}

That looks like a strange thing to hand a shader compiler. Any driver will re-vectorise this in its first pass, so nothing is lost at run time. What is gained is that the emitted text falls inside a grammar small enough to write a reader for — which is what SolidSLGlslTest does. It parses the shader back and evaluates it at tens of thousands of points against SolidExpr.toField3, so the claim "the GLSL computes the same field" is measured rather than asserted from the fact that both came from the same tree walk. A dropped negation, a swapped min and max, an argument in the wrong position: all of those compile perfectly and all of them are caught. Compilation alone — which is where the 2D precedent stops — proves only that the text is valid.

A raymarcher needs to know when the field lies

Sphere tracing steps along the ray by the field's value, which is only safe when that value is a lower bound on the true distance. Several nodes here return something larger than the true distance (SolidExpr.Intersect and SolidExpr.Difference are pointwise maxima; a twisted SolidExpr.Extrude shears space), and stepping by an over-estimate walks a ray straight through a thin wall.

So GlslSolid.stepScale is part of the result rather than a magic number in the shader: the emitter reads fieldNotes and reports the multiplier the march should use. 1.0 means the field is a true distance function and sphere tracing is exact; anything less says how much this particular expression has to be slowed down, and GlslSolid.notes says which node caused it.

src/jvmMain/kotlin/afield/lang/solidsl/SolidGlsl.kt — read the source

Used in: A solid, written down: afield.lang.solidsl

Top-level functions

Declaration Summary
fun SolidExpr.toGlsl(functionName: String = "sdSolid"): GlslSolid This shape as a GLSL distance function.
fun SolidExpr.toRaymarchFragment(maxSteps: Int = 128, functionName: String = "sdSolid"): String A complete fragment shader that draws this shape: sphere tracing, a central-difference normal and two lights, on a flat dark background.

Top-level values

Declaration Summary
const val SOLID_RAYMARCH_VERTEX: String The vertex shader that goes with toRaymarchFragment: one triangle covering the target, its positions taken from gl_VertexIndex, so it needs no vertex buffers and suits fullscreenTriangle.

GlslSolid

data class GlslSolid(val glsl: String, val functionName: String, val statementCount: Int, val stepScale: Double, val notes: List<String>)

The emitted shader, plus what a marcher needs to know about the field it describes.

Declaration Summary
val glsl: String The function source: float <functionName>(vec3 p) { … }.
val functionName: String
val statementCount: Int How many float assignments it took: a cheap proxy for shader cost, and what the simplifier compares before and after.
val stepScale: Double The safe sphere-tracing step, as a fraction of the field's value.
val notes: List<String> SolidExpr.fieldNotes, carried through, because they are exactly what stepScale is derived from.
override fun toString(): String

SolidGradient.kt

Lowering (c): a SolidExpr as a value and an exact gradient — with respect to its own named parameters, not only to position.

val template = (sphere(0.8) + cylinder(1.6, 0.3))
    .parameterize {
        param("r") { it.node == "Sphere" && it.field == "radius" }
        param("h") { it.node == "Cylinder" && it.field == "height" }
    }
    .orThrow()

template.gradientAt(0.9, 0.0, 0.0, doubleArrayOf(0.8, 1.6))
// SolidGradientSample(value = 0.1, dPosition = [1, 0, 0], dParameters = [-1, 0])

Why a third reading

SolidField.kt opens by saying a lambda "cannot be printed, compared, serialized, rewritten, or emitted as GLSL", and this package answered that with two readings that can: toScalarField and exactSolid. Both are evaluations. Neither changes what you can ask the notation for.

afield.lang.rectsl.RectSLFn made the same argument one step further in 2D and is the model here, verbatim: "a track that lowers to an Fn is a track you can take the derivative of, and therefore a track you can solve: declare where a rect should be at some instant and let a gradient find the numbers that put it there." That is what is missing in 3D, and this file plus SolidInverse.kt is it.

The 2D answer does not port. afield.math.functions.Functions.Fn is (Double) -> Double — one input, and a solid is a function of (x, y, z) and of however many named parameters the author declared. The tool that does fit is afield.math.autodiff.AD: reverse-mode automatic differentiation over Double, with the whole elementary set SolidField.kt needs already present. So this lowering is a line-for-line transposition of SolidExpr.toField3 into AD arithmetic, and the two are meant to be read side by side.

Reverse mode, and not forward

The quantity that matters is one scalar loss over many sample points and a handful of parameters. Reverse mode gets every parameter derivative from one forward pass and one backward sweep; forward mode costs one pass per parameter, and autodiff_forward.kt says so itself — its Dual2 is for the opposite shape, one input and many outputs. The position gradient is only three inputs and falls off the same tape for free, so there was never a reason to run two modes.

The differentiable reading is total on the algebra

SolidExact.kt puts every refusal in the type and enumerates five of them. This file has none of that flavour: the differentiable reading is total on the algebra for exactly the reason toScalarField is total on it — it is the same algebra evaluated in a different arithmetic. Every node that has a field form has a differentiable form, because AD is closed under everything Field3 uses.

In fact the shape of it inverts the exact lowering's:

node toScalarField exactSolid this reading
SolidExpr.SmoothUnion native refused — the fillet lies on neither operand native, and C¹: the one boolean with no kink at all
SolidExpr.SmoothDifference native refused native, C¹
SolidExpr.Gyroid native refused — not a bounded volume native; one abs kink on the mid-sheet
SolidExpr.Round native refused — would not converge native, and exactly f − r: the cleanest derivative here
SolidExpr.Union native native subgradient on the seam, split 0.5/0.5

The constructs the exact kernel refuses hardest are the ones this reading is best at. A smooth blend is differentiable by construction; a hard union is not. If you are going to fit a shape, smoothUnion and smoothDifference are the operators to write it with, and SolidExpr.gradientNotes will tell you so about your own expression.

What is refused, always with a stated reason, is binding a parameter to a slot — see SolidExpr.parameterize. They fall into three groups: the address is wrong, two names claim one number, or the seed is singular before the first step — from "that path does not exist, here are the ones that do" to "segments is an integer facet count that toScalarField ignores entirely, so there is no derivative to take". Nothing guesses; a wrong derivative is worse than no derivative.

Kinks and the 0.5/0.5 tie rule

A distance field is full of kinks. min and max are CSG; abs is SolidExpr.Shell and SolidExpr.Gyroid; sqrt at zero is every radial primitive's own centre. A derivative at a kink is a choice, and autodiff.kt's file KDoc has already made it: abs, min and max split the derivative 0.5/0.5 at an exact tie, and it says why — that is "precisely what a central difference reports at a kink, so the analytic gradient and the finite-difference oracle agree there too".

That matters here. It means the CSG seam does not have to be carved out of the finite-difference census: a point sitting exactly on the boundary between two united children is a point where the analytic answer and the numerical oracle still match. The convention is inherited rather than re-chosen, so there is one place it is written down.

Every remaining place where the gradient is a subgradient, is zero when you might have expected otherwise, or is genuinely singular, is enumerated by SolidExpr.gradientNotes — the third notes function in this package, beside SolidExpr.fieldNotes and SolidExpr.allRefusals. The singular ones matter: √(x²+y²+z²) has an infinite position gradient at the origin, so a SolidExpr.Sphere's field is not differentiable at its own centre. Its parameter gradient ∂/∂r = −1 is exact everywhere, which is the asymmetry that makes fitting work anyway.

@see SolidExpr.parameterize to name the numbers you want derivatives with respect to @see afield.lang.solidsl.fitParameters for the inverse problem this exists to serve @see SolidExpr.gradientAgreement for this reading measured against toScalarField, which shares no code with it — SolidAgreement's own argument applied to a third column

src/jvmMain/kotlin/afield/lang/solidsl/SolidGradient.kt — read the source

Used in: A solid, written down: afield.lang.solidsl

Top-level functions

Declaration Summary
fun SolidExpr.numericSlots(): List<SolidNumericSlot> Every Double in this expression that could be a parameter, in traversal order.
fun SolidExpr.withNumericValues(values: Map<String, Double>): SolidLowering<SolidExpr> This expression with the numbers at the given values paths replaced — an ordinary SolidExpr, so the result marches, exact-solids, emits GLSL and prints like anything else in the algebra.
fun SolidExpr.parameterize(spec: SolidParamSpec.() -> Unit): SolidLowering<SolidTemplate> Promote some of this expression's numbers into named parameters, or refuse and say why.
fun SolidExpr.gradientNotes(): List<String> Every place in this expression where the gradient is a subgradient, is structurally zero, or is genuinely singular, in the order they appear — beside fieldNotes and allRefusals as the third disclosure this package owes a reader.
fun SolidExpr.toADField(numerics: SolidNumerics, prefix: String = ""): ADField3 This shape as an ADField3, negative inside — the differentiable transposition of toField3.
fun Profile.toADField2(numerics: SolidNumerics, prefix: String = ""): ADField2 This region as an ADField2, negative inside — the differentiable transposition of toField2.

ADField3

fun interface ADField3

A scalar field over three tracked coordinates — Field3's differentiable twin.

Declaration Summary
fun at(x: AD, y: AD, z: AD): AD

ADField2

fun interface ADField2

A scalar field over two tracked coordinates — Field2's differentiable twin.

Declaration Summary
fun at(x: AD, y: AD): AD

SolidNumerics

class SolidNumerics(private val tape: ADTape, private val bound: Map<String, AD> = emptyMap())

Where a differentiable lowering gets each of an expression's numeric leaves, and the record of which ones it asked for.

Declaration Summary
val pathsAsked: Set<String> Every path this lowering read, in the order it read them.
fun at(path: String, literal: Double): AD The bound parameter at path, or a tape constant holding literal.
fun literal(value: Double): AD A number that is not a slot, and is therefore not recorded: the sentinel an empty subtree reads as.

SolidNumericSlot

data class SolidNumericSlot(val path: String, val node: String, val field: String, val value: Double)

One Double inside an expression that a parameter can be bound to.

Declaration Summary
val path: String
val node: String
val field: String
val value: Double
override fun toString(): String

SolidTemplate

data class SolidTemplate(val base: SolidExpr, val names: List<String>, val slotsOf: Map<String, List<String>>, val seed: DoubleArray)

A SolidExpr plus the named parameters some of its numbers have been promoted into — the thing that can be differentiated with respect to something a human named, and therefore the thing that can be solved.

Declaration Summary
val base: SolidExpr
val names: List<String>
val slotsOf: Map<String, List<String>>
val seed: DoubleArray
val size: Int How many parameters — the length of every vector this template takes and returns.
fun withValues(values: DoubleArray): SolidExpr This template with values substituted, as an ordinary SolidExpr.
fun adFieldWith(parameters: List<AD>): ADField3 The differentiable field with these already-tracked parameters bound to the named slots.
fun gradientAt(x: Double, y: Double, z: Double, values: DoubleArray): SolidGradientSample The value and the whole gradient at one point — position and parameters.
fun report(): String Which numbers these parameters actually reach, for a println in a box.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

SolidGradientSample

data class SolidGradientSample(val value: Double, val dPosition: DoubleArray, val dParameters: DoubleArray)

A field value at a point, and its exact derivatives in both directions that matter.

Declaration Summary
val value: Double The signed distance, negative inside — the same number toScalarField gives.
val dPosition: DoubleArray ∂value/∂(x, y, z) — the surface normal, unnormalised, and unit-length where the field is exact.
val dParameters: DoubleArray ∂value/∂pᵢ for each named parameter, in the template's order.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
override fun toString(): String

SolidParamSpec

class SolidParamSpec internal constructor(val slots: List<SolidNumericSlot>)

The builder SolidExpr.parameterize runs — one param call per name, either against explicit paths or against a predicate over the slot census.

Declaration Summary
val slots: List<SolidNumericSlot> The whole census, so a predicate has something to select from and an error something to list.
fun param(name: String, vararg paths: String, seed: Double? = null) Bind name to exactly these structural paths.
fun param(name: String, seed: Double? = null, where: (SolidNumericSlot) -> Boolean) Bind name to every slot matching where — the form that says "all of them", and therefore the form that expresses a symmetry.

SolidInverse.kt

The payoff of lowering (c): inverse design. Declare what a solid should look like and let a gradient find the numbers that make it so.

val truth  = (sphere(0.80) + cylinder(1.60, 0.30)).let { it }
val target = truth.sampleField(count = 3000, seed = 7)

val template = truth.parameterize {
    param("r") { it.node == "Sphere" && it.field == "radius" }
    param("h") { it.node == "Cylinder" && it.field == "height" }
}.orThrow()

val fit = template.fitParameters(target, start = doubleArrayOf(0.35, 2.60))
println(fit.report())
fit.fitted.march(96)      // an ordinary SolidExpr again

This is afield.lang.rectsl.Track.fitParameters in three dimensions, and it is the reason SolidGradient.kt exists. RectSLFn's KDoc: "a track that lowers to an Fn is a track you can take the derivative of, and therefore a track you can solve: declare where a rect should be at some instant and let a gradient find the numbers that put it there."

The target is a sampled field, not a mesh

A SolidSamples is a bag of points with a signed distance at each. Three things recommend it over fitting against a mesh:

  • It has a gradient. A mesh-to-mesh distance goes through a nearest-triangle search whose result is a discontinuous function of the parameters — the nearest triangle changes — and marching cubes has to be re-run at every step. A field residual is pred − target at a fixed point, and its derivative is exactly what SolidTemplate.adFieldWith hands over.
  • The target need not be a solid at all. sampleField builds one from another SolidExpr, which is what a recovery test wants; but a scan, a level set from somewhere else, or a hand- written list of "this point must be 2mm outside" constraints is the same type.
  • It is cheap and fixed. Sample once; every iteration reads the same array. A 3000-point target costs one march-free forward pass and one backward sweep per step.

The cost is that it fits the field and not the surface, and those are different objectives. Two shapes can have the same zero set and different fields — SolidExpr.Intersect's own SolidExpr.fieldNotes entry says it over-estimates distance outside a concave corner — so a field residual is a stricter target than "look the same". Strictly speaking that is a feature for recovery (there is one right answer and the loss knows it) and a hazard for design (you may not care about the field away from the surface). SolidFitOptions.band is the dial: clamp both sides into a narrow band around zero and the objective becomes almost purely about the surface.

What makes a parameter refuse to move

This is the failure mode of inverse design and it is worth naming before it happens, because it presents as a converged optimisation:

  • A hard SolidExpr.Union gives the whole derivative to whichever child is nearest. A child that is nowhere nearest — swallowed inside another — receives exactly zero from every sample, and its parameters have a zero gradient. Write the shape with smoothUnion and every child contributes within k of the seam.
  • A SolidExpr.Difference cutter entirely inside the body is the same story.
  • A SolidExpr.Empty subtree, or an Extrude sample past the end of the sweep, is a structural zero.

SolidFit.deadParameters reports exactly which parameters had a zero gradient at the start, so that reads as a diagnosis rather than as a mysteriously immediate convergence. And SolidExpr.gradientNotes says the same things about an expression before it is ever fitted.

src/jvmMain/kotlin/afield/lang/solidsl/SolidInverse.kt — read the source

Used in: A solid, written down: afield.lang.solidsl

Top-level functions

Declaration Summary
fun SolidExpr.sampleField(count: Int = 2000, seed: Int = 0, bounds: afield.geometry.spatial.AxisAlignedBoundingBox3D? = null, margin: Double = 0.25): SolidSamples Sample this shape's field at count pseudo-random points in its own bounds, expanded by margin as a fraction of the longest axis.
fun SolidExpr.gradientAgreement(count: Int = 4000, seed: Int = 0, bounds: afield.geometry.spatial.AxisAlignedBoundingBox3D? = null, margin: Double = 0.25): SolidFieldAgreement Evaluate this shape both through toADField and through toScalarField at the same points, and report how far apart they land.
fun SolidTemplate.checkGradient(x: Double, y: Double, z: Double, values: DoubleArray = seed, h: Double = 1e-5): SolidGradientCheck The parameter gradient at one point, beside its central-difference oracle.
fun SolidTemplate.checkPositionGradient(x: Double, y: Double, z: Double, values: DoubleArray = seed, h: Double = 1e-5): SolidGradientCheck checkGradient for the position gradient — the surface normal.
fun SolidTemplate.fitParameters(target: SolidSamples, start: DoubleArray = seed, options: SolidFitOptions = SolidFitOptions()): SolidFit Fit this template's named parameters so its field matches target, by gradient descent on the mean squared distance residual.

SolidSamples

class SolidSamples(val xs: DoubleArray, val ys: DoubleArray, val zs: DoubleArray, val distances: DoubleArray)

A bag of points with a signed distance at each — the thing a parametric solid is fitted to.

Declaration Summary
val xs: DoubleArray
val ys: DoubleArray
val zs: DoubleArray
val distances: DoubleArray
val size: Int
val insideCount: Int How many of these points are inside the shape they were sampled from — a sanity number.
val rms: Double The root-mean-square distance in this target — the scale a loss should be read against.
override fun toString(): String

SolidFieldAgreement

data class SolidFieldAgreement(val expr: SolidExpr, val samples: Int, val maxAbsoluteError: Double, val maxRelativeError: Double, val worstAt: Triple<Double, Double, Double>, val nonFinite: Int)

Lowering (c)'s value against lowering (a)'s, over one shape.

Declaration Summary
val expr: SolidExpr
val samples: Int
val maxAbsoluteError: Double Largest \|c − a\| over the samples, in model units.
val maxRelativeError: Double Largest \|c − a\| / max(1, \|a\|) — relative, but floored so a near-zero value is not a blow-up.
val worstAt: Triple<Double, Double, Double> Where the worst disagreement was, for a println that can be re-evaluated by hand.
val nonFinite: Int How many samples produced a non-finite value on either side — must be 0.
fun report(): String

SolidGradientCheck

data class SolidGradientCheck(val analytic: DoubleArray, val numerical: DoubleArray, val numericalHalfStep: DoubleArray, val step: Double)

An analytic gradient beside a central-difference one, component by component.

Declaration Summary
val analytic: DoubleArray
val numerical: DoubleArray
val numericalHalfStep: DoubleArray The same central difference at half the step — what usableComponents is decided from.
val step: Double
val size: Int How many components there are.
val usableComponents: BooleanArray Which components the numerical oracle is prepared to have an opinion about.
val nonFiniteAnalytic: List<Int> Components where the oracle is trustworthy and the analytic gradient is not a number.
val usableCount: Int How many of them, i.e. how much evidence this check actually carries.
val usable: Boolean Whether there is any verdict here at all.
val maxError: Double The disagreement over the usable components, measured against a floored scale — max(1, \|analytic\|, \|numerical\|).
val worstComponent: Int Which component disagreed the most, or -1 when there is no verdict.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
override fun toString(): String

SolidFitOptions

data class SolidFitOptions(val band: Double? = null, val method: OptimizeMethod = OptimizeMethod.LBFGS, val iterations: Int = 200, val gradientTolerance: Double = 1e-10, val learningRate: Double = 0.05, val memory: Int = 8)

The dials on SolidTemplate.fitParameters.

Declaration Summary
val band: Double?
val method: OptimizeMethod
val iterations: Int
val gradientTolerance: Double
val learningRate: Double
val memory: Int

SolidFit

data class SolidFit(val template: SolidTemplate, val start: DoubleArray, val values: DoubleArray, val initialLoss: Double, val loss: Double, val fitted: SolidExpr, val run: OptimizeRun, val deadParameters: List<String>)

What a fit did, and what it started from.

Declaration Summary
val template: SolidTemplate
val start: DoubleArray Where the descent started.
val values: DoubleArray Where it finished.
val initialLoss: Double The loss at start, measured now.
val loss: Double The loss at values, measured now.
val fitted: SolidExpr The fitted parameters substituted back in — an ordinary SolidExpr.
val run: OptimizeRun The whole descent, for a loss curve or a convergence reason.
val deadParameters: List<String> Parameters whose gradient was exactly zero at start, or that the whole descent left exactly where they began — the ones that could not, or did not, move. The second catches a child that is never the nearest surface anywhere: a cube wholly inside a bar is invisible to the loss, and its parameter comes back bit-identical to the seed.
val improvementFactor: Double initialLoss / loss — how many times better the answer is than the seed.
fun shapeAt(index: Int): SolidExpr The shape the descent was holding at iteration index — an ordinary SolidExpr, marchable like any other.
fun shapesAt(indices: List<Int>): List<SolidExpr> shapeAt for a whole strip, in the order given.
val rmsError: Double The residual as an RMS distance, which is comparable with SolidSamples.rms.
fun report(): String The whole story, for a println in a box.
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

SolidPointCloudFit.kt

Fitting a parametric solid to a point cloud — unoriented samples on a surface, with no signed distances, no normals and no correspondence.

val cloud = someTarget.samplePointCloud(count = 3000, seed = 4, noise = 0.004)
val fit = template.fitToPointCloud(cloud, start = doubleArrayOf(0.3, 1.1, 0.4))

Why a point cloud is a different problem from fitParameters

fitParameters regresses against a SolidSamples — a bag of points with a signed distance at each. That is a rich target and it is also a slightly circular one: the distances come from toScalarField, so the oracle already knows the answer in the same representation the model speaks. It is the right test of whether the gradient works. It is not the problem anybody has.

The problem anybody has is a point cloud: a .psub plane subdivision rendered as points, a compressed-splat PLY, a depth scan. Points on a boundary. No sign, no inside/outside, no normals you trust, no correspondence to anything, some noise, and — nearly always — only the part of the surface that faced the sensor.

The loss is the obvious one, and the three failure modes below all follow from what it does not contain:

L(θ) = (1/N) · Σ f(pᵢ; θ)²

The model's own field, squared, at each cloud point. A surface point should be on the surface, so f should be zero there. That is the whole objective.

The missing interior term

There is no term that wants the interior filled. L is minimised by putting the model's zero level set through the points, and a shape that puts some surface through them scores as well as the shape that generated them. Three consequences, each measured in SolidSLPointCloudTest:

  • A full cloud recovers well. Points all round a closed surface pin it from every side, and the missing interior term does not get a chance to matter.
  • A partial view does not, and it fails in a specific direction. Crop the cloud to one hemisphere and every parameter that only shows itself on the missing side is unconstrained. The loss does not go up when they wander, because there are no points there to notice. This is not a solver failure — the solver finds a genuine minimum of the objective it was given — and no amount of restarting fixes it. It is the objective that is wrong for that data.
  • Shell and Round are dangerous under this loss in a way they are not under fitParameters. abs(f) − t is zero on two surfaces, so a shell can satisfy a cloud with the wrong one of them.

SolidCloudFit.unconstrainedParameters reports the first-order version of the second bullet: a parameter whose gradient stays near zero across the whole cloud is a parameter the data does not see. It is the point-cloud analogue of SolidFit.deadParameters and it exists for the same reason — the failure presents as a converged optimisation.

The target generation shares no code with the model

SolidExpr.samplePointCloud marches the shape with afield.geometry.mesh.marchingCubes and samples its triangles by area. That path touches nothing in SolidGradient.kt: no AD, no tape, no toADField. So a recovery here is two independent implementations agreeing, in the same sense SolidAgreement means it, rather than one implementation checking its own arithmetic.

It is also what a real cloud is like. Triangle-area sampling is uniform over the surface, so (unlike vertex sampling) a coarsely marched flat face is not under-represented relative to a finely marched curved one.

src/jvmMain/kotlin/afield/lang/solidsl/SolidPointCloudFit.kt — read the source

Used in: A solid, written down: afield.lang.solidsl

Top-level functions

Declaration Summary
fun SolidExpr.samplePointCloud(count: Int = 3000, seed: Int = 0, noise: Double = 0.0, resolution: Int = 110): SolidPointCloud Sample count points uniformly over this shape's surface, by marching it and sampling its triangles by area.
fun TriMesh.samplePoints(count: Int, seed: Int = 0, noise: Double = 0.0): SolidPointCloud count points scattered uniformly over this mesh's triangles, by area.
fun SolidTemplate.fitToPointCloud(cloud: SolidPointCloud, start: DoubleArray, options: SolidFitOptions = SolidFitOptions(method = OptimizeMethod.LBFGS, iterations = 500)): SolidCloudFit Fit this template's named parameters so its surface passes through cloud, by gradient descent on the mean squared field value.

SolidPointCloud

class SolidPointCloud(val xs: DoubleArray, val ys: DoubleArray, val zs: DoubleArray)

Unoriented points on a surface — the target SolidTemplate.fitToPointCloud takes.

Declaration Summary
val xs: DoubleArray
val ys: DoubleArray
val zs: DoubleArray
val size: Int
fun extent(): DoubleArray The cloud's axis-aligned extent, as (minX, minY, minZ, maxX, maxY, maxZ).
val scale: Double The longest axis of extent — the scale a residual should be read against.
fun filter(keep: (Double, Double, Double) -> Boolean): SolidPointCloud This cloud with only the points keep accepts — how a partial view is made.
override fun toString(): String

SolidCloudFit

data class SolidCloudFit(val template: SolidTemplate, val start: DoubleArray, val values: DoubleArray, val initialLoss: Double, val loss: Double, val fitted: SolidExpr, val run: OptimizeRun, val unconstrainedParameters: List<String>)

What a point-cloud fit did, and what the cloud could not tell it.

Declaration Summary
val template: SolidTemplate
val start: DoubleArray
val values: DoubleArray
val initialLoss: Double Mean squared field value over the cloud at start, measured now.
val loss: Double The same at values.
val fitted: SolidExpr
val run: OptimizeRun
val unconstrainedParameters: List<String> Parameters whose gradient was negligible across the whole cloud at the start, or that the descent never moved off their seed — the ones this data cannot see.
val improvementFactor: Double initialLoss / loss.
val rmsDistance: Double The RMS field value over the cloud — an approximate distance from the cloud to the fitted surface, in model units, and the number to read as "how far off is it".
fun report(): String
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

SolidSimplify.kt

Rewriting a SolidExpr into a smaller one that means the same thing — the other half of what making the 3D family symbolic bought, beside a second and third backend.

val messy = unionOf(listOf(sphere(1), sphere(1))).at(0, 0, 0).at(1, 0, 0) - SolidExpr.Empty
println(messy.simplified())        // Translate(child=Sphere(radius=1.0, segments=0), x=1.0, …)

None of these rewrites is available to a closure. Two sdfSphere(1.0) lambdas are different objects and always will be; two SolidExpr.Sphere(1.0) are ==. Nothing can ask a (Vec3) -> Float whether it is ever non-zero, or where; a node knows its own SolidExpr.bounds without sampling. This file is what those two facts are worth.

The rules

The dull ones first, because a notation accumulates them the moment anyone generates code into it: identity transforms drop out, adjacent SolidExpr.Translates and SolidExpr.Scales fold, nested booleans of the same kind flatten into one n-ary node (which matters more here than in most algebras, because n-ary is exactly what reaches Manifold's batch boolean rather than a fold), and SolidExpr.Empty is absorbed.

Two of them are worth more than that:

  • Disjoint operands. a ∩ b where the two bounds do not overlap is SolidExpr.Empty; a − b where b is nowhere near a is just a. That is a fact about the expression, decided from symbolic bounds with nothing sampled and nothing built. On a generated assembly — a hundred bores drilled through one plate of which twelve actually meet it — this is the difference between a shader that runs and one that does not.

  • A vanishing blend removes a refusal. SmoothUnion(a, b, k) with k at or below zero is a plain union, and a plain union has an exact CSG form where a smooth blend never will. So simplification can move an expression from the level-set fallback onto the exact path, which is a strange and rather nice thing for a peephole optimiser to be able to do. simplified is therefore worth calling before buildSolid and not only before toGlsl.

What it does not do

It does not compose two SolidExpr.Rotates unless they turn about the same single axis. Two general Euler rotations compose to a matrix, and a matrix does not always come back out as three Euler angles without a gimbal case to get wrong — and the whole discipline of this package is that a rewrite which is nearly right is worse than no rewrite. It also does not reassociate SolidExpr.SmoothUnion, which is genuinely non-associative, or reorder a SolidExpr.Difference's operands, which is genuinely non-commutative.

Every rule is checked by sampling: SolidSLSimplifyTest asserts that the simplified expression's field agrees with the original's at thousands of points, for every rule and every combination it has. A rewrite that changes the shape is the failure this file exists to rule out, so it is the most heavily sampled.

src/jvmMain/kotlin/afield/lang/solidsl/SolidSimplify.kt — read the source

Used in: A solid, written down: afield.lang.solidsl

Top-level functions

Declaration Summary
fun SolidExpr.simplified(maxPasses: Int = 8): SolidExpr This expression, rewritten smaller. Bottom-up, then repeated until it stops changing — a fixed point, because flattening a boolean can expose a fold that exposes an identity.
fun Profile.simplified(maxPasses: Int = 8): Profile simplified for a 2D region.
fun SolidExpr.nodeCount(): Int How many nodes this expression has — the number simplified is trying to reduce.
fun SolidExpr.simplificationReport(): String A one-line account of what simplified did to this expression, for a println in a box.

StackSLBox.kt

Host-side entry points for StackSL: compiling the stack language to GLSL, and evaluating it on the GPU sample by sample. The language, its compiler and its diagnostics live in afield.lang.stacksl; this file is the Kotlin surface a box calls.

stackSL holds a box's StackSL source, falling back to a parent box's. compileStackSL, stackSLToGlsl and the standalone slGlsl are pure compilation and need no device. slEval and slSweep actually run the compiled function: they wrap it in a small compute shader and dispatch it through a Renderer that defaults to fromContext(), so they need a live graphics context. Neither binds any texture sampler, so a source that needs one is rejected.

slSweep returns a StackSLSweep, which implements ToHTML and draws itself as an inline SVG plot of the four output channels with per-channel min/median/max; StackSLSweep.h is that HTML.

src/jvmMain/kotlin/afield/lang/stacksl/StackSLBox.kt — read the source

Used in: StackSL cheatsheet, StackSL examples

Top-level functions

Declaration Summary
fun Box.stackSLPrefix(): String A GLSL-safe identifier prefix unique to this box — stacksl_ plus the first 12 characters of the box id with - turned into _ (stacksl_box for a box with a blank id).
fun Box.compileStackSL(source: String = stackSL ?: "", prefix: String = stackSLPrefix(), entrypoint: String = "main", sampleTextures: List<String> = listOf("source"), tolerant: Boolean = false, declareSamplers: Boolean = true, externalizeConstants: Boolean = false): StackSLCompileResult Compile this box's StackSL to GLSL and hand back the whole result — generated source, diagnostics and the static analysis (stack depths, required helpers and samplers, dangling wires). This is the one to call when you care whether the program is actually valid.
fun Box.stackSLToGlsl(source: String = stackSL ?: "", prefix: String = stackSLPrefix(), entrypoint: String = "main", sampleTextures: List<String> = listOf("source"), tolerant: Boolean = false, declareSamplers: Boolean = true, externalizeConstants: Boolean = false): String compileStackSL with the diagnostics thrown away — just the GLSL, for pasting into a shader template or a stack.auto slot. Same defaults, same box-derived stackSLPrefix.
fun slEval(source: String, input: Vector4fc, renderer: Renderer = fromContext()): Vec4 Run one StackSL program on one input vector and get the vec4 it leaves on the stack — the "what does this word actually do" probe, and how the StackSL examples check their arithmetic.
fun slGlsl(source: String, prefix: String = "stacksl", entrypoint: String = "main", sampleTextures: List<String> = listOf("source"), tolerant: Boolean = false, declareSamplers: Boolean = true, externalizeConstants: Boolean = false): String Compile a StackSL string to GLSL with no box and no device in sight — the standalone twin of Box.stackSLToGlsl, for looking at the code a program generates.
fun slSweep(source: String, range: Pair<Vector4fc, Vector4fc>, samples: Int = 100, renderer: Renderer = fromContext()): StackSLSweep Evaluate a StackSL program across a line of inputs and return the whole response as a plottable StackSLSweep — the direct way to see what a word does to a range rather than to one number.

Top-level values

Declaration Summary
var Box.stackSL: String? This box's StackSL source text, the property every helper here defaults to. Inherited, so a box that has none falls back to the nearest ancestor that does — a parent can hold the program and its children compile variations of it.
val StackSLSweep.h: String The sweep's StackSLSweep.toHTML plot, for display() and anywhere else HTML is wanted.

StackSLSweep

data class StackSLSweep(val source: String, val start: Vec4, val end: Vec4, val inputs: List<Vec4>, val values: List<Vec4>) : ToHTML

What slSweep measured: the inputs it used and the vec4 the program returned for each, kept side by side so the response can be plotted or asserted on.

Declaration Summary
val source: String The StackSL program that was evaluated, exactly as it was passed in.
val start: Vec4 The first input vector of the sweep — range.first as given to slSweep.
val end: Vec4 The last input vector of the sweep; it is evaluated, the range being closed at both ends.
val inputs: List<Vec4> Every input vector actually dispatched, in order, from start to end inclusive.
val values: List<Vec4> The program's output for each entry of inputs, index for index. May contain NaN or infinity.
val size: Int How many samples the sweep holds — the samples slSweep was asked for.
operator fun get(index: Int): Vec4 The output for sample index; throws IndexOutOfBoundsException past size, as a list does.
fun channel(channel: Int): List<Float> One output channel across the whole sweep, as a list ready for a plot: 0 is x/r, 1 y/g, 2 z/b, 3 w/a. Any other index throws IllegalArgumentException. Non-finite samples are handed back as they are — it is the plot, not this, that filters them.
override fun toHTML(): String The sweep drawn as a self-contained SVG plot: one polyline per output channel, dots and guide lines on each channel's extremes, the first and last input/output pair, and a per-channel avg/min/median/max table.
override fun toString(): String A one-line summary — sample count plus the first and last output — for logs and assertions.

StackSLFeedback.kt

StackSL with its own previous frame: Box.stackFeedback draws a box's stackSL program fullscreen every frame into a float ping-pong target, with the last frame bound as sampler2D state, so prev, sample, gather and the store words (0-> … 3->) read what the program drew a frame ago. StackSLFeedback is the running host; StackSLLowering is the panel it shows in the box's output — the lowered GLSL, its helpers, wires, stack depth and diagnostics. StackSLFeedbackTest covers the shader it builds and the panel.

src/jvmMain/kotlin/afield/lang/stacksl/StackSLFeedback.kt — read the source

Top-level functions

Declaration Summary
fun Box.stackFeedback(renderer: Renderer = fromContext(), reset: Boolean = true, configure: StackSLFeedbackSettings.() -> Unit = {}): StackSLFeedback Draw this box's StackSL fullscreen with its previous frame as state: the StackTrinket renderer in one call. The box needs a renderer above it, and its program in the stackSL property.

Top-level values

Declaration Summary
const val STACKSL_LOWERING_CHANNEL The output channel Box.stackFeedback shows its StackSLLowering in.
const val STACKSL_GLSL_CHANNEL The output channel Box.stackFeedback shows the lowered GLSL in; it starts closed.

StackSLFeedbackSettings

class StackSLFeedbackSettings

Settings for a StackSLFeedback. All but the size can change while it runs.

Declaration Summary
var name: String Names what the host puts in the window. Every feedback box in a document uses the same name by default, so launching one replaces whichever was showing — the boxes work as a preset menu.
var width: Int The state's size in pixels; 0 takes the window's. Fixed once the host is built.
var height: Int
var fps: Double How many times a second the program steps. A feedback program's decays are per step (prev 0.965 * keeps 96.5% each frame), so its look depends on this rate, and the window may draw far faster than a browser does; 60 is what StackTrinket gets in a browser at 60 Hz. 0 steps on every frame the window draws.
var timeScale: Double Seconds of program time per second of wall time — what time and 0.1t count in.
var paused: Boolean Hold the picture: no frame is drawn and time stops until this is false again.
var displayGamma: Double The exponent the window applies to what the program drew. A browser shows a WebGL canvas's numbers as sRGB, and Field's window expects linear light, so 2.2 shows a program the way the StackTrinket page and the StackSL gallery show it. The state itself is never changed by this.
var showLowering: Boolean Show the StackSLLowering panel in the box's lowering output channel.
var viewport: Rect? Where in the window the picture goes; null fills it.
val inputs: MutableMap<String, HasImageView> More pictures the program can read beside state, in order: the first is sample1 / gather1 / texelSize1, the second sample2, up to three. Any texture will do — a classmate's WebRTC stream (webrtcSource), a camera, another render target. Each is copied, the right way up and at the state's size, once per step, so 0 sample1 reads it at this pixel with y up, exactly as prev reads the last frame. See input.
fun input(name: String, texture: HasImageView) Add texture as the next input, read by sample1, sample2, …; name is its GLSL sampler name.

StackSLFeedback

class StackSLFeedback internal constructor(val renderer: Renderer, val settings: StackSLFeedbackSettings, var source: () -> String, private val onCompiled: (StackSLLowering) -> Unit) : Target

A StackSL program running against its own previous frame — the StackTrinket renderer.

Declaration Summary
val renderer: Renderer
val settings: StackSLFeedbackSettings
var source: () -> String Where the program comes from; read every frame.
val width: Int Width of the state in pixels.
val height: Int Height of the state in pixels.
val target The two images: one is drawn while the other is state.
var lowering: StackSLLowering? The last compile, good or bad.
var running: String? The program the running pipeline was built from.
var frame: Long Frames drawn since the last reset.
var seconds: Double Program time, in seconds, since the last reset.
val picture: HasImageView The picture as Field's other textures keep one: top row first, 8-bit display values (what the window shows, before its display gamma). Hand it to anything that samples a texture — webrtcPublish(feedback.picture, "stacktrinket") publishes the program to the class. The state itself is stored bottom row first and in float, which suits prev but nothing else. Made the first time it is asked for, and updated on every step after that.
override fun toString(): String
fun reset() Clear both images to black and start time and the frame count again from zero.
fun show(): ModulePointer Put the picture in the window, replacing whatever was shown under the same name.
override fun render(command: CommandBuffer, index: Int)

StackSLFeedbackShader

object StackSLFeedbackShader

The shaders a StackSLFeedback draws with: a fullscreen triangle, and a fragment shader that hands the compiled program vec4(uv, hash, seconds) with the previous frame bound as STATE.

Declaration Summary
const val STATE The sampler the previous frame is bound as, and the texture StackSL's sample words read.
const val PREFIX The GLSL name of the compiled program.
val compileOptions: StackSLCompileOptions How a feedback program is compiled: sampling STATE, with its numbers as dangling wires.
fun compileOptions(inputs: List<String>) compileOptions with inputs as the textures sample1, sample2, … read.
val PICTURE_VERTEX: String The fullscreen triangle as Field draws one elsewhere: fragUV.y = 1 lands in row 0.
val PICTURE_FRAGMENT: String Reads the state at fragUV; drawn with PICTURE_VERTEX it comes out top row first.
val COPY_FRAGMENT: String Copies an input texture into an image stored like the state: drawn with VERTEX, it reads source with v flipped, because Field's textures keep their top row first. With encode set (an sRGB source, such as a WebRTC stream's browser paints) it re-encodes what sampling decoded, so the program reads the numbers the picture holds — the values a browser ShaderTrinket's iChannel gives, and the space state is in. Without it a stream reads darker, by the gamma.
val VERTEX: String The fullscreen-triangle vertex shader: fragUV runs 0..1 with y up, and the image is written so that image row 0 holds uv.y = 0. That is the row texture(state, vec2(u, 0)) reads, so prev reads back this pixel. Field's render targets put clip y = +1 in row 0, so the usual gl_Position = pos * 2 - 1 would store the picture upside down relative to its own sampling and every feedback step would flip it; StackSLFeedback.show flips it back for the window.
fun fragment(lowered: String, inputs: List<String> = emptyList()): String The fragment shader around lowered, the GLSL the compiler made for a program. params is (seconds, frame, width, height); a negative frame draws black, which is how the state is cleared. The hash is StackTrinket's, so a program's grain looks as it does there.

StackSLLowering

class StackSLLowering(val source: String, val result: StackSLCompileResult, val glslError: String?, val running: Boolean) : ToHTML

What one compile of a feedback program came to, as StackTrinket's lowering panel shows it: whether it is drawing, the stack depth with the leftover values under the returned top, the helpers it pulled in, how many dangling wires (tunable numbers) it has, each diagnostic at its line and column; glslHtml is the lowered GLSL line by line. It is a ToHTML, so display("lowering", it) draws the summary.

Declaration Summary
val source: String
val result: StackSLCompileResult
val glslError: String? Why the GPU refused GLSL the compiler accepted, or null.
val running: Boolean Whether a program is drawing — this one if it compiled, otherwise the last good one.
val ok: Boolean The program compiled and the GPU took it.
val depth: Int How many values the program leaves on the stack; the top is what is drawn.
val guidance: String StackTrinket's one-line reading of depth.
val numberedGlsl: String The lowered GLSL with three-digit line numbers, as the panel shows it.
override fun toHTML(): String
val glslHtml: String The numbered GLSL as HTML, scrolling inside a fixed 500 by 300 box. It is a panel of its own (STACKSL_GLSL_CHANNEL): a panel is sized to all of its content, folded or not, so inside the summary it held the summary open at full height.

stackAutoNumbers.kt

Live numbers for a {{ stack.auto.foo }} slot: every literal in the StackSL becomes a member of a uniform block, foo_numbers, that sliders, Kotlin or a keyframe can turn without a recompile.

StackAutoNumbers holds the block's layout and the calls that seed it and switch it live. StackAutoNumbersTest covers it.

src/jvmMain/kotlin/afield/lang/stacksl/stackAutoNumbers.kt — read the source

StackAutoNumbers

object StackAutoNumbers

The numbers in a {{ stack.auto.foo }} slot, live on the GPU: each number typed in the StackSL is compiled as a member of a uniform block of the slot's own, foo_numbers, so it can be turned without a recompile — from the box's sliders, from Kotlin, or from a keyframe.

Declaration Summary
const val CAPACITY How many numbers a slot's block holds.
fun blockName(slot: String) The uniform block a slot's numbers live in.
fun member(slot: String, index: Int) The block member holding number index of slot.
fun liveMember(slot: String) The block member that turns slot's numbers on.
fun isNumbersBlock(name: String, members: Collection<String>) Whether a uniform block name with these members is some slot's numbers: foo_numbers, holding foo_live.
data class Number(val index: Int, val initial: Double, val wire: StackSLDanglingWire) One number of a slot as compiled: its index, the literal it was compiled from and where it is.
class Slot(val box: Box, val name: String) A slot of a box as last compiled: its source, its numbers (empty when baked), and whether it has gone live.
fun slots(box: Box): Map<String, Slot> box's slots, by name, as last compiled.
fun slot(box: Box, name: String): Slot box's slot name, created on first use.
val onCompiled Listeners told when a slot of a box compiles with new source — the box's card redraws.
fun binding(slot: String, shaderTexts: List<String>): Int The binding slot's block takes in set 0: 20 on, one per slot, in the order the slots first appear across shaderTexts (all of the box's stages, so every stage agrees), skipping any binding the texts already declare.
fun block(slot: String, binding: Int): String slot's uniform block, at binding in set 0: foo_live and CAPACITY numbers, whatever the program.
fun compileLive(slot: String, source: String, sampleTexture: String, entrypoint: String?): Pair<String, StackSLCompileResult>? source compiled for slot with its numbers read from the slot's block: the function a template expands to (after the block), and the compile; null when the program has more than CAPACITY numbers (compile it baked). A failed compile comes back with an empty function.
fun compiled(box: Box, slot: String, source: String, entrypoint: String?, wires: List<StackSLDanglingWire>) Record that box's slot compiled source with these wires; a live slot whose source changed is re-seeded from it.
fun baked(box: Box, slot: String, source: String) Record that box's slot compiled with its numbers baked in (too many, or not a stage that can hold them).
fun structures(box: Box, slot: String): List<Structure> The structures box's modules hold for slot's block, one per module that declares it.
var moduleStructures: (Box) -> List<Map<String, Structure>> Every live-uniform map of the modules box owns, found through the resolver Field registers when it builds them.
fun value(box: Box, slot: String, index: Int): Double? Number index of box's slot as the GPU sees it: what was set, else its literal.
fun live(box: Box, slot: String) Make box's slot live — every member seeded from what was set or the literals, foo_live on — without changing a number.
fun set(box: Box, slot: String, index: Int, value: Double) Turn number index of box's slot to value, on every module that holds the slot, live from now on.
fun reset(box: Box, slot: String) Put every number of box's slot back to its literal.
fun created(structures: Map<String, Structure>) A module has just made its uniform structures: each numbers block's foo_live is written 0, so its numbers read as their literals. A uniform buffer starts as whatever memory it was given; left unwritten, foo_live was garbage, and a slot nobody had touched drew black.
fun carry(box: Box?, rebuild: afield.graphics.render.ModuleRebuild.Rebuild) A module of box (null when its owner is not known) was rebuilt, and the generic carry-over has already run (afield.graphics.render.ModuleRebuild): it moved every value and keyframe binding of a numbers block that still fits, and dropped the rest. This only fills in after it, for a live slot: a block the generic carry had nothing for is seeded whole; a carried block gets, for each number it left without a value or binding, what was set on the slot or its literal. Nothing the generic carry put there is overwritten.

modelling.kt

A self-tuning controller for pushing the distribution of a scalar signal towards a target shape. You hand AdaptiveDistributionController.pushSamples a batch of samples each frame and it returns a ControlOutput whose appliedParams are the next parameter values to send to whatever produced them — a shader, a simulation, a tone curve.

The error signal comes from a FeatureExtractor; UniformQuantileFeatureExtractor is the built-in one and measures how far a set of sample quantiles sit from their targets. Internally each feature gets a recursive-least-squares row (with forgetting) that learns a local Jacobian, the step is a Tikhonov-regularised Gauss-Newton solve scaled by a confidence estimate, then softened near the parameter bounds, rate-limited by maxStep, clipped, and finally offset by a per-parameter sinusoidal dither so the estimator keeps seeing excitation.

ControllerConfig.observationDelayFrames is the important knob: it models pipeline latency explicitly by pairing each observation with the command issued that many pushes earlier, so the loop does not have to be slowed down to learn correctly. Use AdaptiveDistributionControllerBank for one controller per scene, and AdaptiveDistributionLoop to drive observe/apply from a Runloop hook.

This is plain DoubleArray math with no GPU or Box dependency; every array in ControllerConfig must be parameterNames.size long. main runs a synthetic drifting-Gaussian plant against the controller and prints a trace.

src/jvmMain/kotlin/afield/math/control/modelling.kt — read the source

Top-level functions

Declaration Summary
fun ControlOutput.params(useNominalParams: Boolean = false): DoubleArray
fun ControlOutput.namedAppliedParams(parameterNames: List<String>): Map<String, Double>
fun ControlOutput.namedNominalParams(parameterNames: List<String>): Map<String, Double>
fun main()

FeatureExtractor

interface FeatureExtractor

Declaration Summary
val featureCount: Int
fun errorFromSamples(samples: DoubleArray): DoubleArray

UniformQuantileFeatureExtractor

class UniformQuantileFeatureExtractor(private val targetQuantiles: DoubleArray = doubleArrayOf(0.1, 0.3, 0.5, 0.7, 0.9)) : FeatureExtractor

Declaration Summary
override val featureCount: Int
override fun errorFromSamples(samples: DoubleArray): DoubleArray

ControllerConfig

data class ControllerConfig(val parameterNames: List<String> = listOf("offset", "logContrast", "logGamma", "cubic", "skew"), val paramMin: DoubleArray = doubleArrayOf(-0.35, -1.2, -1.1, -1.2, -1.2), val paramMax: DoubleArray = doubleArrayOf(+0.35, +1.2, +1.1, +1.2, +1.2), val initialParams: DoubleArray = DoubleArray(5), val maxStep: DoubleArray = doubleArrayOf(0.0030, 0.0100, 0.0080, 0.0100, 0.0100), val ditherAmplitude: DoubleArray = doubleArrayOf(0.0035, 0.0100, 0.0080, 0.0100, 0.0100), val ditherOmega: DoubleArray = doubleArrayOf(0.070, 0.111, 0.091, 0.133, 0.053), val ditherPhase: DoubleArray = doubleArrayOf(0.0, 1.4, 2.2, 0.7, 2.8), val forgetting: Double = 0.997, val initialCovariance: Double = 20.0, val gainBase: Double = 0.45, val tikhonov: Double = 0.18, val emaAlpha: Double = 0.10, val clipPenalty: Double = 0.25, val observationDelayFrames: Int = 3, val covarianceConfidenceScale: Double = 0.06, val residualConfidenceScale: Double = 2.0, val minConfidence: Double = 0.05, val boundarySoftZone: Double = 0.15)

Declaration Summary
val parameterNames: List<String>
val paramMin: DoubleArray
val paramMax: DoubleArray
val initialParams: DoubleArray
val maxStep: DoubleArray
val ditherAmplitude: DoubleArray
val ditherOmega: DoubleArray
val ditherPhase: DoubleArray
val forgetting: Double
val initialCovariance: Double
val gainBase: Double
val tikhonov: Double
val emaAlpha: Double
val clipPenalty: Double
val observationDelayFrames: Int
val covarianceConfidenceScale: Double
val residualConfidenceScale: Double
val minConfidence: Double
val boundarySoftZone: Double
val parameterCount: Int

ControllerDiagnostics

data class ControllerDiagnostics(val pushIndex: Int, val learnedFromCommandIndex: Int, val rawFeatureError: DoubleArray, val filteredFeatureError: DoubleArray, val objective: Double, val clipRate: Double, val confidence: Double, val residualRatio: Double, val averageVariance: Double, val pairedAppliedParams: DoubleArray)

Declaration Summary
val pushIndex: Int
val learnedFromCommandIndex: Int
val rawFeatureError: DoubleArray
val filteredFeatureError: DoubleArray
val objective: Double
val clipRate: Double
val confidence: Double
val residualRatio: Double
val averageVariance: Double
val pairedAppliedParams: DoubleArray

ControlOutput

data class ControlOutput(val nominalParams: DoubleArray, val appliedParams: DoubleArray, val diagnostics: ControllerDiagnostics)

Declaration Summary
val nominalParams: DoubleArray
val appliedParams: DoubleArray
val diagnostics: ControllerDiagnostics

ScalarSampleBatch

data class ScalarSampleBatch(val samples: DoubleArray, val clipRate: Double = 0.0)

Declaration Summary
val samples: DoubleArray
val clipRate: Double

AdaptiveDistributionController

class AdaptiveDistributionController(private val config: ControllerConfig, private val featureExtractor: FeatureExtractor)

Declaration Summary
fun currentNominalParams(): DoubleArray
val parameterNames: List<String>
val parameterCount: Int
fun pushSamples(samples: DoubleArray, clipRate: Double = 0.0): ControlOutput
fun pushFeatureError(rawFeatureError: DoubleArray, clipRate: Double = 0.0): ControlOutput

AdaptiveDistributionControllerBank

class AdaptiveDistributionControllerBank<K>(private val factory: () -> AdaptiveDistributionController)

Declaration Summary
fun get(sceneKey: K): AdaptiveDistributionController
fun pushSamples(sceneKey: K, samples: DoubleArray, clipRate: Double = 0.0): ControlOutput
fun pushFeatureError(sceneKey: K, rawFeatureError: DoubleArray, clipRate: Double = 0.0): ControlOutput

AdaptiveDistributionLoop

class AdaptiveDistributionLoop(val controller: AdaptiveDistributionController, private val observe: () -> ScalarSampleBatch?, private val apply: (ControlOutput) -> Unit, private val onUpdate: ((ScalarSampleBatch, ControlOutput) -> Unit)? = null)

Declaration Summary
val controller: AdaptiveDistributionController
var tickCount: Int
var lastBatch: ScalarSampleBatch?
var lastOutput: ControlOutput?
fun tick(): ControlOutput?
fun installRunloopHook(key: String, runloop: Runloop = Runloop.runLoop): AdaptiveDistributionLoop
fun removeRunloopHook(key: String, runloop: Runloop = Runloop.runLoop): AdaptiveDistributionLoop

quantilewarp.kt

Reshapes a scalar signal's distribution by fitting a monotone 8-knot PCHIP curve from the measured quantiles of recent samples onto the quantiles of a chosen target distribution — TargetDistributions.UnitNormal by default, or any QuantileFunction.

AdaptiveQuantileWarp.update takes a batch of samples, merges it with the last historyFrames batches, and returns a QuantileWarpState: the knot pairs, an QuantileWarpState.evaluate for CPU-side use, and the same 16 knots packed into four vec4s ready for push constants via QuantileWarpState.applyToPushConstants. Successive states are smoothed towards each other by smoothingAlpha and forced strictly increasing, so the curve stays invertible even when the input degenerates.

fieldQuantileWarpIncludeSource is the matching GLSL — fieldQuantileWarp8 and fieldQuantileWarpMix evaluate the identical PCHIP curve on the GPU — and is reachable from shader source as the built-in include named by FIELD_QUANTILE_WARP_INCLUDE_NAME. Box.fieldProbeQuantileWarp closes the loop: it installs a runloop hook that drains a ShaderFieldProbe, updates the warp, calls your apply callback and renders a knot table into a box display channel.

Two constraints: exactly eight strictly increasing probabilities inside (0, 1), and Box.fieldProbeQuantileWarp needs a live Box plus a probe configured for continuous snapshot reads.

src/jvmMain/kotlin/afield/math/statistics/quantilewarp.kt — read the source

Used in: Measuring dependence between two signals, Field 4 Graphics

Top-level functions

Declaration Summary
fun Box.fieldProbeQuantileWarp(probe: ShaderFieldProbe, warp: AdaptiveQuantileWarp = AdaptiveQuantileWarp(), config: ShaderProbeQuantileWarpConfig = ShaderProbeQuantileWarpConfig(), install: Boolean = true, runloop: Runloop = Runloop.runLoop, apply: (warp: QuantileWarpState, batch: FieldProbeScalarBatch) -> Unit): ShaderFieldQuantileWarpController Close the loop: let a shader's own output decide how that shader's values are scaled, every frame.
fun inverseStandardNormalCdf(p: Double): Double The standard normal quantile function Φ⁻¹(p) — Acklam's rational approximation, in three branches that meet at p = 0.02425 and its reflection.

Top-level values

Declaration Summary
const val FIELD_QUANTILE_WARP_INCLUDE_NAME The name shader source uses to pull in the GLSL half of this file: #include "quantileWarp".
var Box._fieldProbeQuantileWarpController: ShaderFieldQuantileWarpController? The controller Box.fieldProbeQuantileWarp installed on this box, if any.

QuantileFunction

fun interface QuantileFunction

The inverse CDF of the distribution you want a signal reshaped into: given a probability, the value at that quantile. A SAM interface, so QuantileFunction { p -> ... } is enough.

Declaration Summary
fun quantile(p: Double): Double The value at probability p. Called only with p strictly inside (0, 1).

TargetDistributions

object TargetDistributions

The ready-made QuantileFunction targets: UnitNormal (the default target of AdaptiveQuantileWarp), Uniform01, and uniform for an arbitrary range.

Declaration Summary
val Uniform01 The identity: warp a signal into a flat [0, 1] distribution. Accepts the closed range and throws outside it.
val UnitNormal Standard normal, mean 0 and standard deviation 1 — the default target, and the one that makes a skewed signal usable with symmetric thresholds. Rejects p at exactly 0 or 1.
fun uniform(min: Double, max: Double) A flat distribution over [min, max] — the target to use when a shader wants a value it can index or threshold in known bounds.

QuantileWarpConfig

data class QuantileWarpConfig(val probabilities: DoubleArray = defaultQuantileWarpProbabilities.copyOf(), val historyFrames: Int = 8, val smoothingAlpha: Double = 0.20, val epsilonRelative: Double = 1.0e-5, val epsilonAbsolute: Double = 1.0e-6)

How AdaptiveQuantileWarp fits: which eight quantiles to measure, how much history to pool, and how fast to move.

Declaration Summary
val probabilities: DoubleArray The eight quantiles to measure. Exactly eight, strictly increasing, strictly inside (0, 1).
val historyFrames: Int How many update batches are pooled before the quantiles are taken. At least 1; 1 means fit on the current batch alone.
val smoothingAlpha: Double How far each update moves the knots towards the newly measured ones, in [0, 1].
val epsilonRelative: Double Minimum gap between adjacent knots as a fraction of the knot range; keeps the curve invertible.
val epsilonAbsolute: Double Minimum absolute gap between adjacent knots — the floor that survives a signal collapsing to a constant, where the relative term is zero. Must be strictly positive.

QuantileWarpState

class QuantileWarpState(probabilities: DoubleArray, inputKnots: DoubleArray, targetKnots: DoubleArray, val historySampleCount: Int)

One fitted warp: eight inputKnots measured from the signal, eight targetKnots from the target distribution, and the monotone PCHIP curve through them.

Declaration Summary
val historySampleCount: Int How many pooled samples these quantiles were measured from.
val probabilities: DoubleArray The eight probabilities the knots were measured at. A copy.
val inputKnots: DoubleArray The measured value of the signal at each probability — the x axis of the curve. Strictly increasing.
val targetKnots: DoubleArray The target distribution's value at each probability — the y axis of the curve.
val packedPushConstants: FloatArray The sixteen numbers a shader needs: eight inputKnots then eight targetKnots, as Float.
fun evaluate(x: Double): Double The warp itself: a signal value in, its position in the target distribution out.
fun xLo(): Vector4f Input knots 0..3, ready for a vec4 push constant.
fun xHi(): Vector4f Input knots 4..7.
fun yLo(): Vector4f Target knots 0..3.
fun yHi(): Vector4f Target knots 4..7.
fun applyToPushConstants(setVec4: (name: String, value: Vector4f) -> Unit, xLoName: String = "warpXLo", xHiName: String = "warpXHi", yLoName: String = "warpYLo", yHiName: String = "warpYHi") Push all four vec4s through one setVec4 callback, under the names the GLSL include expects.
override fun toString(): String The sample count and both knot arrays to four decimal places — a warp you can read in a log line.

AdaptiveQuantileWarp

class AdaptiveQuantileWarp(private val config: QuantileWarpConfig = QuantileWarpConfig(), target: QuantileFunction = TargetDistributions.UnitNormal)

Reshapes a signal's distribution into a target one, adaptively: hand update a batch of samples and get back a QuantileWarpState whose QuantileWarpState.evaluate maps that signal onto the target — TargetDistributions.UnitNormal unless you pass another.

Declaration Summary
fun currentStateOrNull(): QuantileWarpState? The last state update produced, or null before the first one. Does not recompute anything.
fun reset(seed: QuantileWarpState? = null): AdaptiveQuantileWarp Forget the pooled history and the current state, optionally starting again from seed.
fun update(samples: DoubleArray): QuantileWarpState Fold a batch of samples in and return the new warp.

ShaderProbeQuantileWarpConfig

data class ShaderProbeQuantileWarpConfig(val minSamples: Int = 128, val clipLower: Double? = null, val clipUpper: Double? = null, val clipEpsilon: Double = 1.0e-6, val displayChannel: String? = "warp", val closeDisplayOnStop: Boolean = false, val clearDisplayOnStop: Boolean = false, val runloopKeySuffix: String = "shaderFieldQuantileWarp")

How Box.fieldProbeQuantileWarp drives a warp off a GPU probe: when a batch is big enough to be worth fitting, what to clip, and where to draw the knots.

Declaration Summary
val minSamples: Int Fewest probe samples worth fitting on. A tick that cannot gather this many does nothing at all rather than fitting a worse curve.
val clipLower: Double? Discard samples at or below this before fitting; null keeps everything. The usual reason is a background value that would otherwise own the lower knots.
val clipUpper: Double? Discard samples at or above this before fitting; null keeps everything.
val clipEpsilon: Double Tolerance used when comparing against the clip bounds, so a sample exactly on the bound is decided consistently. Must be strictly positive.
val displayChannel: String? Box display channel for the live knot table, or null to draw nothing.
val closeDisplayOnStop: Boolean Close the display channel when the controller stops, rather than leaving the last table up.
val clearDisplayOnStop: Boolean Blank the display channel when the controller stops.
val runloopKeySuffix: String Suffix of the runloop hook key, which is "<suffix>:<box id>". Change it only to run two warps on one box.

ShaderFieldQuantileWarpController

class ShaderFieldQuantileWarpController(val box: Box, val probe: ShaderFieldProbe, val warp: AdaptiveQuantileWarp, val config: ShaderProbeQuantileWarpConfig, private val apply: (warp: QuantileWarpState, batch: FieldProbeScalarBatch) -> Unit)

The running loop: drain a ShaderFieldProbe, refit the warp, hand it to your apply callback, draw the knots.

Declaration Summary
val box: Box The box that owns the runloop hook and the display channel.
val probe: ShaderFieldProbe Where the samples come from. Must be configured for continuous snapshot reads.
val warp: AdaptiveQuantileWarp The fitter being driven. Its config decides the pooling and smoothing.
val config: ShaderProbeQuantileWarpConfig How this controller reads the probe and what it draws.
var tickCount: Int How many times a new probe snapshot has been fitted — not how many frames have passed.
var lastBatch: FieldProbeScalarBatch? The most recent batch that was fitted, or null if none has been yet.
var lastWarp: QuantileWarpState? The warp fitted from lastBatch, or null if none has been yet.
fun tick(): QuantileWarpState? One attempt: read the probe, and if there is a new snapshot with enough samples, refit and apply.
fun install(runloop: Runloop = Runloop.runLoop): ShaderFieldQuantileWarpController Add the pre-run hook that calls tick every frame. Idempotent.
fun stop(runloop: Runloop = Runloop.runLoop, closeDisplay: Boolean = config.closeDisplayOnStop, clearDisplay: Boolean = config.clearDisplayOnStop): ShaderFieldQuantileWarpController Remove the hook, optionally tidy the display, and forget this controller on the box.

ShaderTransitionNotes.kt

Notes and markers that report how a shader reload changed a box's uniforms, buffers and push constants.

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

Top-level functions

Declaration Summary
fun markShaderReferenceLines(box: Box, names: Map<String, String>): Int Put an error marker on each line of box's code that uses one of names — keyed by the shader name, valued by why it no longer means what it did — with a suggestion to re-run. At most a dozen lines per name, so a common member name cannot paint the whole box. Returns how many were marked.

ShaderTransitionNotes

object ShaderTransitionNotes

What the last full shader reload of a box did to its interface, said where the person editing it is looking.

Declaration Summary
fun summary(box: Box, property: String): String? The summary the last reload of box recorded against property, or null.
fun record(box: Box, properties: Collection<String>, transition: ShaderInterfaceTransition?, carryNotes: List<String> = emptyList()): String Record transition as what the reload just did to box's shader properties (the properties the person edits — fragment, or fragment_s, or the shared vertex_fragment), log its summary, and mark the lines of code that use a name it made stale. carryNotes are the values the reload could not carry. Returns the summary, "" when nothing changed.

FLineInspect.kt

Make a figure answerable: inspect draws FLines the way lines does and then names whatever you click on, in the figure's own coordinates.

The problem it solves is the one the panel documents ran into — a figure was drawn, and then the numbers about it were fished back out with println because there was no way to ask the picture "which edge is this, and how far along it am I?". inspect wires FLine.onClick and FLine.hoverColor for every line it is given, so a click reports the element under the pointer (see FLineIntersection) and writes a readable card into a second output channel.

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

Top-level functions

Declaration Summary
fun Box.inspect(lines: List<FLine>, channel: String = "inspect", labelChannel: String = "$channel — what you clicked", w: Double = -1.0, h: Double = -1.0, fitToSize: Boolean = true, hover: Vec4? = vec(1.0, 0.84, 0.32, 0.9), onHit: ((FLineIntersection) -> Unit)? = null, describe: (FLineIntersection) -> String = { it.describe() }): List<FLine> Draw lines and report what gets clicked.
fun Box.inspect(vararg lines: FLine, channel: String = "inspect", labelChannel: String = "$channel — what you clicked", w: Double = -1.0, h: Double = -1.0, fitToSize: Boolean = true, hover: Vec4? = vec(1.0, 0.84, 0.32, 0.9), onHit: ((FLineIntersection) -> Unit)? = null, describe: (FLineIntersection) -> String = { it.describe() }): List<FLine> inspect for a handful of lines written out one by one.
fun FLineIntersection.describe(): String One line of prose for a hit: what was clicked, and where on it.

pointing_render.kt

Seeing what the pointer found: the picked part lit, a crosshair where the pointer was aimed, and a caption that names what it hit.

Why this is in the layer and not left to the caller

Because "it compiles" and "a hit record came back non-null" are both worthless as evidence for something whose whole job is to tell a person what they are touching. The failure mode is not a crash; it is the wrong part lit, or a pointer that is confidently off by the height of a caption band. Neither shows up in a unit test's return value, and both are obvious in a picture.

So the layer ships its own way of drawing itself, SpatialPointerPixelTest decodes the PNG back out and reads samples from it, and the demo transcript uses the same call a person would.

What the picture is

TriMesh.renderPng's rasterizer, over a mesh whose per-vertex colours have been repainted by part (TriMesh.coloredByPart), with a crosshair composited on afterwards at the pointer's pixel. All of it CPU: no GPU, no window, no AWT — so it runs inside a live Field box, inside a test, and inside a headless docs capture, identically.

val scene = pickScene { add("model", mesh, parts = builder.groups()) }
val camera = MeshView().rayCamera(scene.mergedMesh())
val at = Vec2(360f, 270f)
display(scene.pointerFigureHtml(camera, at))

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

Top-level functions

Declaration Summary
fun TriMesh.coloredByPart(parts: PickParts, palette: (part: Int) -> Vec3): TriMesh This mesh with each triangle painted by the part it belongs to.
fun TriMesh.withPartHighlight(parts: PickParts, part: Int, highlight: Vec3 = PART_HIGHLIGHT, base: Vec3 = PART_BASE): TriMesh This mesh with one part lit and everything else neutral — the picture of a pick.
fun TriMesh.coloredByPart(parts: PickParts): TriMesh This mesh with every part its own colour, walked around a hue circle.
fun partPalette(part: Int): Vec3 The default colour for a part id: a golden-ratio walk around the hue circle, so adjacent part ids are never adjacent colours and a five-part model does not come out as five shades of the same green. PickParts.UNASSIGNED gets a dead grey.
fun PickScene.renderPointer(camera: MeshViewRayCamera, at: Vec2, view: MeshView = MeshView(width = camera.pixelWidth, height = camera.pixelHeight), crosshairRadius: Int = 11, maxDistance: Double = Double.MAX_VALUE): PointerRaster Render this scene from camera with whatever is under the pointer lit up and a crosshair where the pointer is aimed.
fun PickScene.pointerFigureHtml(camera: MeshViewRayCamera, at: Vec2, view: MeshView = MeshView(width = camera.pixelWidth, height = camera.pixelHeight), title: String? = null): String The pick as a <figure> — the picture, and a caption naming the part — for display(…) in a box.
fun Box.showPointer(scene: PickScene, camera: MeshViewRayCamera, at: Vec2, view: MeshView = MeshView(width = camera.pixelWidth, height = camera.pixelHeight), title: String? = null): PickHit? Push pointerFigureHtml to a box output — the one-liner a live document uses.
fun PickScene.clickableProjection(camera: MeshViewRayCamera, highlightPart: Int = PickParts.UNASSIGNED, clickHandler: (browser: afield.graphics.render.FLineIntersection, jvm: PickHit?) -> Unit = { _, _ -> }): List<afield.geometry.path.FLine> This scene projected to 2D as one filled, clickable FLine per visible triangle — a real mouse pointer, and the bridge between the JVM picker in this layer and the browser picker Field already had.

PointerRaster

class PointerRaster(val pixels: IntArray, val width: Int, val height: Int, val hit: PickHit?, val pointerX: Double, val pointerY: Double)

A finished picture of a pick: ARGB pixels, the size, and what the pointer found.

Declaration Summary
val pixels: IntArray ARGB, row-major, width x height.
val width: Int
val height: Int
val hit: PickHit? What the pointer found, or null.
val pointerX: Double Where the pointer was aimed, in pixels.
val pointerY: Double
fun toPng(): ByteArray pixels as PNG bytes, via afield.media.image.util.PngEncoder. No AWT.
fun caption(): String One line naming what was hit, for a caption or a log.