RectSLWorkout.kt
src/jvmMain/kotlin/afield/examples/rectsl/RectSLWorkout.kt · 809 lines
package afield.examples.rectsl
import afield.lang.rectsl.*
import afield.math.functions.Functions
import afield.math.functions.evolve
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.cos
import kotlin.math.sin
object RectSLWorkout
FIELD_TEST id: rectsl.language.workout runner: jvm-main entrypoint: afield.examples.rectsl.RectSLWorkout tags: [smoke, examples, rectsl, animation, geometry, stacksl] timeoutSec: 30 expect: exitCode: 0 stdoutContains: - "rectsl workout passed"
object RectSLWorkout {
private const val EPS = 1e-6
fun main(args: Array<String>)
@JvmStatic
fun main(args: Array<String>) {
check(runTests()) { "rectsl workout failed" }
}
fun runTests(): Boolean
body — 777 lines, from line 32
fun runTests(): Boolean {
val tests = listOf(
"tokenizer" to ::tokenizerParsesSurface,
"minimal mirror mesh" to ::minimalMirrorMesh,
"definitions and groups" to ::definitionsAndGroups,
"selections and deep timing transforms" to ::selectionsAndDeepTimingTransforms,
"operators and key lenses" to ::operatorsAndKeyLenses,
"track modes and time warps" to ::trackModesAndTimeWarps,
"track pair separators" to ::trackPairSeparators,
"layers tracks easing and repeat" to ::layersTracksEasingAndRepeat,
"array primitives" to ::arrayPrimitives,
"functions bridge" to ::functionsBridge,
"gradient and material lowering" to ::gradientAndMaterialLowering,
"semantic analysis" to ::semanticAnalysis,
"StackSL material shader" to ::stackSLMaterialShader
)
tests.forEach { (name, test) ->
try {
test()
println("ok - $name")
} catch (t: Throwable) {
println("failed - $name: ${t.message}")
t.printStackTrace()
return false
}
}
println("rectsl workout passed")
return true
}
private fun tokenizerParsesSurface() {
val tokens = RectLangTokenizer(
"""
# comments are ignored
: white 0xffffff solid ->fill ;
"voice-a" rect
'( 0 -12 8 0 18 12 ) ->x
;
""".trimIndent()
).tokenize()
require(tokens.any { it is Token.StringLit && it.value == "voice-a" })
require(tokens.any { it is Token.ColorLit })
val quote = tokens.filterIsInstance<Token.Quote>().single()
require(quote.tokens.filterIsInstance<Token.Number>().size == 6) { quote }
require(tokens.filterIsInstance<Token.Word>().any { it.name == ":" })
require(tokens.filterIsInstance<Token.Word>().any { it.name == "->fill" })
val metadata = RectLangVm().wordMetadata().associateBy { it.spelling }
require(metadata.getValue("->xy").effect == RectWordEffect.Mutator)
require(metadata.getValue("+").effect == RectWordEffect.Ordinary)
require("xy" !in metadata) { "unprefixed mutators must not remain registered aliases" }
val unprefixedMutator = RectAnimationOracle.compile("\"r\" rect 0 0 xy ;")
require(!unprefixedMutator.ok)
require(
unprefixedMutator.diagnostics.single() ==
"Unknown word: xy (mutators use '->'; did you mean '->xy'?)"
) { unprefixedMutator.diagnostics }
val trailingQuote = RectAnimationOracle.compile(
"""
"r" rect
1 ->square
;
"
""".trimIndent()
)
require(!trailingQuote.ok) { "Trailing single quote should not compile" }
require(trailingQuote.diagnostics.single().contains("exactly three quotes")) {
trailingQuote.diagnostics
}
// An unknown word (typo) reports what it saw *and* suggests the closest real
// word — never a bare NullPointerException from `.program!!`.
val typo = RectAnimationOracle.compile(
"""
"bar" rect 0 0 ->xy 1 ->square ;
"bar" 4 '( i 0 ->xy ) arra
""".trimIndent()
)
require(!typo.ok) { "Unknown word 'arra' should not compile" }
require(typo.diagnostics.single() == "Unknown word: arra (did you mean 'area' or 'array'?)") {
"expected an unknown-word suggestion, got: ${typo.diagnostics}"
}
val npe = runCatching { typo.program }.exceptionOrNull()
require(npe is RectLangException && npe.message?.contains("Unknown word: arra") == true) {
".program on a failed compile should throw the diagnostic, not an NPE; got $npe"
}
}
private fun minimalMirrorMesh() {
val program = compile(
"""
->wall 30 4 meters
->duration 10
->fps 60
"r" rect
-5 0 ->xy
1 ->square
0xff3300 solid ->fill
'( 0 -5 5 0 10 5 ) ->x
;
dup "r2" ->rename
->mirror-x
0x3333ff solid ->fill
;
""".trimIndent()
)
val rects = program.nodes.filterIsInstance<RectNode>()
require(rects.map { it.id }.toSet() == setOf("r", "r2")) { rects.map { it.id } }
val at0 = program.sample(0.0).rects.associateBy { it.id }
val at5 = program.sample(5.0).rects.associateBy { it.id }
val at10 = program.sample(10.0).rects.associateBy { it.id }
near(-5.0, at0.getValue("r").center.x)
near(5.0, at10.getValue("r").center.x)
near(5.0, at0.getValue("r2").center.x)
near(-5.0, at10.getValue("r2").center.x)
near(0.0, at5.getValue("r").center.x)
near(0.0, at5.getValue("r2").center.x)
val mesh = lowerSampledScene(program.sample(5.0))
require(mesh.vertices.size == 8) { "Expected 8 vertices, got ${mesh.vertices.size}" }
require(mesh.indices.size == 12) { "Expected 12 indices, got ${mesh.indices.size}" }
require(mesh.vertices.all { it.position.z == 0f }) { "RectSL mesh positions should stay inside clip-space z" }
require(mesh.vertices.map { it.material.z }.toSet() == setOf(0f, 1f)) { "Expected object ids in material.z" }
require(mesh.vertices.all { it.material.x == 0f }) { "Expected plain material id" }
require(mesh.vertices.all { it.shaderData == Vec4f.ZERO }) { "Expected zero shader data" }
}
private fun definitionsAndGroups() {
val program = compile(
"""
->wall 30 4 meters
->duration 12
->fps 24
: white
0xffffff solid ->fill
;
: soft
0.25 ->alpha
;
: voice-bar
rect
white
soft
2.0 0.2 ->wh
;
"group-low" group
"low-a" voice-bar
-2 -1 ->xy
;
"low-b" voice-bar
2 -1 ->xy
;
end
"group-low" ->select
10 0 ->translate
0.1 ->rot
2 ->scale
;
""".trimIndent()
)
val sampled = program.sample(0.0).rects.associateBy { it.id }
val lowA = sampled.getValue("low-a")
val expectedA = Vec2d(10.0, 0.0) + rotate(Vec2d(-4.0, -2.0), 0.1)
near(expectedA.x, lowA.center.x)
near(expectedA.y, lowA.center.y)
near(0.1, lowA.rotation)
near(2.0, lowA.scale.x)
near(2.0, lowA.scale.y)
near(0.25, lowA.alpha)
near(2.0, lowA.size.x)
near(0.2, lowA.size.y)
require(program.nodes.filterIsInstance<GroupNode>().single().children == listOf("low-a", "low-b"))
}
private fun selectionsAndDeepTimingTransforms() {
val program = compile(
"""
->wall 12 4 meters
->duration 4
->fps 24
"source" group
"voice-a" rect
"voice" ->tag
0 0 ->xy
1 ->square
'( 0 0 4 4 ) linear ->x
;
"drone-a" rect
"drone" ->tag
2 0 ->xy
1 ->square
'( 0 2 4 6 ) linear ->x
;
end
"source" ->select
clone-deep "copy" ->rename
;
"copy" ->select descendants
"voice" tagged
'( 1 ->delay-all-time 0.5 ->opacity* ) each
;
""".trimIndent()
)
val groups = program.nodes.filterIsInstance<GroupNode>().associateBy { it.id }
require(groups.getValue("source").children == listOf("voice-a", "drone-a"))
require(groups.getValue("copy").children.toSet() == setOf("voice-a-copy", "drone-a-copy")) {
groups.getValue("copy").children
}
val at2 = program.sample(2.0).rects.associateBy { it.id }
near(2.0, at2.getValue("voice-a").center.x)
near(1.0, at2.getValue("voice-a-copy").center.x)
near(0.5, at2.getValue("voice-a-copy").alpha)
near(4.0, at2.getValue("drone-a").center.x)
near(4.0, at2.getValue("drone-a-copy").center.x)
near(1.0, at2.getValue("drone-a-copy").alpha)
}
private fun operatorsAndKeyLenses() {
val program = compile(
"""
->wall 12 4 meters
->duration 6
->fps 24
"voice-a" rect
"voice" ->tag
0 0 ->xy
1 2 + ->square
'( 0 0 2 2 4 4 ) linear ->x
'( 0 1 2 0.3 4 1.4 ) linear ->alpha
;
"other" rect
4 0 ->xy
1 ->square
'( 0 0 2 2 4 4 ) linear ->x
;
"voice-a" ->select
x-track 1 time+ 2 value* ->x
alpha-track 0 1 clamp-values ->alpha
;
"other" ->select
x-track 0 2 keys-between 10 value+ ->x
;
all rects
'( "voice" tagged? id "other" = or ) filter
'( 0.5 ->opacity* ) each
;
""".trimIndent()
)
val at3 = program.sample(3.0).rects.associateBy { it.id }
val voice = at3.getValue("voice-a")
val other = at3.getValue("other")
near(4.0, voice.center.x)
near(3.0, voice.size.x)
near(3.0, voice.size.y)
require(voice.alpha in 0.32..0.33) { voice.alpha }
near(12.0, other.center.x)
near(0.5, other.alpha)
}
private fun trackModesAndTimeWarps() {
val program = compile(
"""
->wall 10 4 meters
->duration 8
->fps 30
"tracks" rect
'( 0 0 4 8 ) linear ->x
'( 0 1 4 3 ) hold ->y
'( 0 1 4 3 ) tcb -0.4 0 0 tcb! ->scale
'( 0 0 2 1 4 0 ) ->alpha
;
"delayed" rect
'( 0 0 2 2 ) tcb 1 delay-time ->x
0 ->y
1 ->square
;
""".trimIndent()
)
val at2 = program.sample(2.0).rects.associateBy { it.id }
val tracks = at2.getValue("tracks")
near(4.0, tracks.center.x)
near(1.0, tracks.center.y)
require(tracks.scale.x > 1.0 && tracks.scale.x < 3.0) { tracks.scale }
require(tracks.alpha > 0.5) { tracks.alpha }
val delayedAt1 = program.sample(1.0).rects.associateBy { it.id }.getValue("delayed")
val delayedAt3 = program.sample(3.0).rects.associateBy { it.id }.getValue("delayed")
near(0.0, delayedAt1.center.x)
near(2.0, delayedAt3.center.x)
}
private fun trackPairSeparators() {
// `time=value` is sugar for a plain space between the two numbers of a key.
// It must tokenize into the same keys, mix freely with spaces, and leave the
// comparison operators (`=`, `<=`, `!=`, `tcb!`) untouched elsewhere.
val program = compile(
"""
->wall 10 4 meters
->duration 4
->fps 30
hold-track pulse '( 0=0.2 2=1.0 4=0.45 )
"r" rect
0 0 ->xy
1 ->square
'( 0=-5 2 0 4=5 ) linear ->x
pulse ->alpha
;
""".trimIndent()
)
val r0 = program.sample(0.0).rects.single()
val r2 = program.sample(2.0).rects.single()
val r4 = program.sample(4.0).rects.single()
// hold-track pulse, referenced as alpha.
near(0.2, r0.alpha)
near(1.0, r2.alpha)
near(0.45, r4.alpha)
// mixed `=` / space linear x track.
near(-5.0, r0.center.x)
near(0.0, r2.center.x)
near(5.0, r4.center.x)
// Operators containing `=` still tokenize as single words, and `=` is only a
// separator inside track quotes — not a general split (`tcb!` stays whole).
val ops = RectLangTokenizer("<= >= != <> = tcb! 0=0.2").tokenize()
val words = ops.filterIsInstance<Token.Word>().map { it.name }
require(words == listOf("<=", ">=", "!=", "<>", "=", "tcb!", "=")) { words }
val nums = ops.filterIsInstance<Token.Number>().map { it.value }
require(nums == listOf(0.0, 0.2)) { nums }
}
private fun layersTracksEasingAndRepeat() {
val program = compile(
"""
->wall 8 4 meters
->duration 2
->fps 24
linear-track enter '( 0 -2 2 2 )
track lift '( 0 0 2 2 )
"background" rect
0 0 ->xy
3 ->square
"background" ->layer
;
"foreground" rect
0 0 ->xy
2 ->square
"foreground" ->layer
;
4 '(
rect
"bar" ->tag
i 1.5 * -2.25 + -1 ->xy
0.25 ->square
lift smooth ->z
;
) repeat
all '( "bar" tagged? ) where "bars" ->group-selected
1 0 ->translate
;
"mover" rect
enter ease-in-out ->x
1 ->y
0.5 ->square
->front
;
0 3 '(
rect
"range" ->tag
i 0.5 * -0.5 + 1.5 ->xy
0.2 ->square
->back
;
) range
""".trimIndent()
)
val groups = program.nodes.filterIsInstance<GroupNode>().associateBy { it.id }
require(groups.getValue("bars").children.size == 4) { groups.getValue("bars").children }
val at0 = program.sample(0.0).rects
val at05 = program.sample(0.5).rects.associateBy { it.id }
val at1 = program.sample(1.0).rects
require(at0.first().tags.contains("range")) { at0.map { it.id to it.z } }
require(at0.last().id == "mover") { at0.map { it.id to it.z } }
near(-1.5, at05.getValue("mover").center.x)
val barsAt1 = at1.filter { "bar" in it.tags }.sortedBy { it.center.x }
require(barsAt1.size == 4) { barsAt1.map { it.id } }
near(-1.25, barsAt1.first().center.x)
barsAt1.forEach { near(1.0, it.z) }
val foreground = at0.first { it.id == "foreground" }
val background = at0.first { it.id == "background" }
require(foreground.z > background.z) { at0.map { it.id to it.z } }
}
private fun arrayPrimitives() {
// array: element 0 is the template; ic centers the run; f drives alpha across the array.
val linear = compile(
"""
->wall 20 8 meters
->duration 1
->fps 1
"cell" rect
0 0 ->xy
1 ->square
5 '( ic 2 * 0 ->offset 0 1 f mix ->alpha ) array
;
""".trimIndent()
)
val cells = linear.sample(0.0).rects
require(cells.size == 5) { "array expected 5 rects, got ${cells.size}" }
val xs = cells.map { it.center.x }.sorted()
listOf(-4.0, -2.0, 0.0, 2.0, 4.0).forEachIndexed { i, e -> near(e, xs[i]) }
val alphas = cells.sortedBy { it.center.x }.map { it.alpha }
listOf(0.0, 0.25, 0.5, 0.75, 1.0).forEachIndexed { i, e -> near(e, alphas[i]) }
// radial: four copies placed on a circle of radius 3 at 0, 90, 180, 270 degrees.
val ring = compile(
"""
->wall 12 12 meters
->duration 1
->fps 1
"spoke" rect
0.5 ->square
4 3 '( ) radial
;
""".trimIndent()
)
val spokes = ring.sample(0.0).rects
require(spokes.size == 4) { "radial expected 4 rects, got ${spokes.size}" }
val byId = spokes.sortedBy { it.center.x }
near(-3.0, byId.first().center.x)
near(3.0, byId.last().center.x)
require(spokes.any { near2(it.center.x, 0.0) && near2(it.center.y, 3.0) }) { spokes.map { it.center } }
require(spokes.any { near2(it.center.x, 0.0) && near2(it.center.y, -3.0) }) { spokes.map { it.center } }
// grid: cols x rows fresh cells, centered with colc/rowc; returns a selection to tag as a batch.
val grid = compile(
"""
->wall 20 12 meters
->duration 1
->fps 1
3 2 '(
"g" rect
colc 2 * rowc 2 * ->xy
0.5 ->square
;
) grid
'( "cell" ->tag ) each
""".trimIndent()
)
val gridRects = grid.sample(0.0).rects
require(gridRects.size == 6) { "grid expected 6 rects, got ${gridRects.size}" }
require(gridRects.all { "cell" in it.tags }) { "grid selection tag did not broadcast" }
require(gridRects.any { near2(it.center.x, -2.0) && near2(it.center.y, -1.0) }) { gridRects.map { it.center } }
require(gridRects.any { near2(it.center.x, 2.0) && near2(it.center.y, 1.0) }) { gridRects.map { it.center } }
// stagger: cumulative positional offset over a selection returned by array.
val staggered = compile(
"""
->wall 20 8 meters
->duration 1
->fps 1
"s" rect
0 0 ->xy
0.5 ->square
3 '( ) array
1 0 ->stagger
;
""".trimIndent()
)
val stag = staggered.sample(0.0).rects.map { it.center.x }.sorted()
listOf(0.0, 1.0, 2.0).forEachIndexed { i, e -> near(e, stag[i]) }
// array of a group deep-clones the whole subtree.
val groups = compile(
"""
->wall 24 8 meters
->duration 1
->fps 1
"tile" group
"a" rect -0.5 0 ->xy 0.4 ->square ;
"b" rect 0.5 0 ->xy 0.4 ->square ;
end
"tile" ->select
3 '( ic 5 * 0 ->offset ) array
""".trimIndent()
)
require(groups.nodes.filterIsInstance<GroupNode>().size == 3) {
"array of a group expected 3 groups, got ${groups.nodes.filterIsInstance<GroupNode>().size}"
}
groups.nodes.filterIsInstance<GroupNode>().forEach {
require(it.children.size == 2) { "each cloned group should keep 2 children, got ${it.children.size}" }
}
val groupRects = groups.sample(0.0).rects
require(groupRects.size == 6) { "group array expected 6 child rects, got ${groupRects.size}" }
// centered ic offsets -5,0,5 shift each group; child "a" sits at group.x-0.5.
require(groupRects.any { near2(it.center.x, -5.5, 1e-4) }) { groupRects.map { it.center.x } }
require(groupRects.any { near2(it.center.x, 5.5, 1e-4) }) { groupRects.map { it.center.x } }
}
private fun near2(actual: Double, expected: Double, epsilon: Double = 1e-4): Boolean =
abs(actual - expected) <= epsilon
private fun functionsBridge() {
// #2 ease — a per-copy eased alpha matches the Functions Penner curve exactly.
val eased = compile(
"""
->wall 20 8 meters
->duration 1
->fps 1
"c" rect 0 0 ->xy 1 ->square
5 '( ic 2 * 0 ->offset f "quad-out" ease ->alpha ) array
;
""".trimIndent()
)
val alphas = eased.sample(0.0).rects.sortedBy { it.center.x }.map { it.alpha }
val expectedEase = with(Functions) {
val e = identity().ease(Functions.Easing.QUAD_OUT)
(0..4).map { e.eval(it / 4.0) }
}
alphas.zip(expectedEase).forEach { (a, e) -> near(e, a) }
// #1/#4 sample — a gaussian distribution sampled at f peaks in the middle and
// falls off symmetrically toward the edges of the array.
val bell = compile(
"""
->wall 20 8 meters
->duration 1
->fps 1
"c" rect 0 0 ->xy 1 ->square
5 '( ic 2 * 0 ->offset gaussian f sample ->alpha ) array
;
""".trimIndent()
)
val bs = bell.sample(0.0).rects.sortedBy { it.center.x }.map { it.alpha }
near(1.0, bs[2])
require(bs[0] < 0.05 && bs[4] < 0.05) { "gaussian edges should be dark: $bs" }
near(bs[0], bs[4])
// #4 seeded noise — deterministic for a seed, and different seeds diverge.
fun scatter(seed: Int) = compile(
"""
->wall 20 8 meters
->duration 1
->fps 1
"c" rect 0 0 ->xy 1 ->square
7 '( ic 1.5 * 0 ->offset $seed noise f sample sat ->alpha ) array
;
""".trimIndent()
).sample(0.0).rects.sortedBy { it.center.x }.map { it.alpha }
require(scatter(1) == scatter(1)) { "seeded noise must be deterministic" }
require(scatter(1) != scatter(2)) { "different seeds must diverge" }
// #3 reflect — a curve transform: reflect(x) == source(1-x).
val plain = compile(
"""
->wall 4 4 meters
->duration 1
->fps 1
"c" rect 0 0 ->xy 1 ->square 3 noise 0.2 sample sat ->alpha ;
""".trimIndent()
).sample(0.0).rects.first().alpha
val reflected = compile(
"""
->wall 4 4 meters
->duration 1
->fps 1
"c" rect 0 0 ->xy 1 ->square 3 noise reflect 0.8 sample sat ->alpha ;
""".trimIndent()
).sample(0.0).rects.first().alpha
near(plain, reflected)
// #5 graph/evolve — a RectSL-built curve keeps its Functions graph, so the
// evolve tooling can optimise an array's distribution toward a target.
val fn = (evalRectSLStack("3 noise reflect").last() as VFn).fn
require(fn.graph != null) { "the Fn bridge dropped the Functions graph (needed for rewrite/evolve)" }
val target = 0.5
val run = evolve(seed = fn, iterations = 24, rngSeed = 1L) { c -> -abs(c.eval(0.5) - target) }
require(run.bestScore >= run.initialScore) { "evolve regressed fitness" }
require(run.best.eval(0.5).isFinite()) { "evolved curve is not samplable" }
// #5 injection — the host binds a curve by name; the RectSL array samples it.
val curve = with(Functions) { gaussian(0.5, 0.16) }
val injected = compileRectSL(
"""
->wall 20 8 meters
->duration 1
->fps 1
"c" rect 0 0 ->xy 1 ->square
5 '( ic 2 * 0 ->offset shape f sample ->alpha ) array
;
""".trimIndent(),
mapOf("shape" to curve)
).programOrNull ?: error("compileRectSL with curves failed")
val ia = injected.sample(0.0).rects.sortedBy { it.center.x }.map { it.alpha }
near(1.0, ia[2])
require(ia[0] < 0.05 && ia[4] < 0.05) { "injected curve did not drive the array: $ia" }
}
private fun gradientAndMaterialLowering() {
val program = compile(
"""
->wall 6 4 meters
->duration 1
->fps 1
material pulse '( uv time + )
"gradient" rect
0 0 ->xy
2 2 ->wh
0xff0000 0x0000ff -1 0 1 0 linear ->fill
soft-edge ->material
0.12 2 3 4 ->shader
;
"stacked" rect
2 0 ->xy
1 ->square
0xffffff solid ->fill
pulse ->material
;
""".trimIndent()
)
val scene = program.sample(0.0)
val mesh = lowerSampledScene(scene)
require(mesh.vertices.size == 8)
val gradientVertices = mesh.vertices.take(4)
require(gradientVertices[0].color.x > gradientVertices[1].color.x) { gradientVertices.map { it.color } }
require(gradientVertices[1].color.z > gradientVertices[0].color.z) { gradientVertices.map { it.color } }
require(gradientVertices.all { it.material.x == 1f }) { "soft-edge material id expected" }
near(0.12, gradientVertices.first().shaderData.x.toDouble())
val stackedVertices = mesh.vertices.drop(4)
require(stackedVertices.all { it.material.x == 3f }) { stackedVertices.map { it.material } }
require(program.materialTable.modules.single().name == "pulse")
}
private fun semanticAnalysis() {
val program = compile(
"""
->wall 4 4 meters
->duration 4
->fps 20
"left" rect
-1 0 ->xy
1 ->square
'( 0 -1 2 0 4 3 ) ->x
'( 0 0 2 1 4 0 ) ->alpha
;
dup "right" ->rename
->mirror-x
;
""".trimIndent()
)
val report = analyzeAnimation(program, AnalysisSettings(fps = 20.0))
require(report.events.any { it is VisualEvent.TouchStart }) { report.events }
require(report.events.any { it is VisualEvent.TouchEnd }) { report.events }
require(report.events.any { it is VisualEvent.LeaveScreen }) { report.events }
require(report.events.any { it is VisualEvent.OpacityCrossing && it.threshold == 0.5 && it.rising }) {
report.events
}
require(report.objectMetrics.keys.containsAll(listOf("left", "right"))) { report.objectMetrics.keys }
require(report.frameMetrics.any { it.visibleObjectCount == 2 }) { report.frameMetrics.take(5) }
}
private fun stackSLMaterialShader() {
val program = compile(
"""
material slow-stripes '(
uv 8.0 * time + sin
0.5 * 0.5 +
)
"stripe" rect
0 0 ->xy
4 0.35 ->wh
0xffcc66 solid ->fill
slow-stripes ->material
9 0 0.15 0 ->shader
;
""".trimIndent()
)
val compiled = compileStackSLMaterials(program)
require(compiled.size == 1)
require(compiled.single().ok) { compiled.single().stackSL.diagnostics.joinToString("\n") }
val insertion = rectMaterialShaderInsertion(compiled)
require("#version" !in insertion) { insertion }
require("void main()" !in insertion) { insertion }
require("struct RectFrag" in insertion) { insertion }
require("vec4 material_slow_stripes(RectFrag r)" in insertion) { insertion }
require("if (id == 3) return material_slow_stripes(r);" in insertion) { insertion }
require("shadePlain" in insertion && "shadeSoftEdge" in insertion && "shadeDiagnostic" in insertion)
val shaderCode = rectMaterialShaderCode(program)
require(shaderCode.ok) { shaderCode.diagnostics.joinToString("\n") }
require(shaderCode.insertion == insertion) { shaderCode.insertion }
require("#version 450" in shaderCode.exampleFragmentShader) { shaderCode.exampleFragmentShader }
require("void main()" in shaderCode.exampleFragmentShader) { shaderCode.exampleFragmentShader }
val shader = rectAnimationFragmentShader(compiled)
require(shader == shaderCode.exampleFragmentShader) { shader }
}
private fun compile(source: String): SceneProgram {
val result = RectAnimationOracle.compile(source)
require(result.ok) { result.diagnostics.joinToString("\n") }
return result.program
}
private fun rotate(p: Vec2d, angle: Double): Vec2d =
Vec2d(
cos(angle) * p.x - sin(angle) * p.y,
sin(angle) * p.x + cos(angle) * p.y
)
private fun near(expected: Double, actual: Double, epsilon: Double = EPS) {
require(abs(expected - actual) <= epsilon) {
"Expected $expected, got $actual"
}
}
}