RectSLPrograms.kt
src/jvmMain/kotlin/afield/lang/rectsl/study/RectSLPrograms.kt · 694 lines
package and imports — 57 lines, from line 1
package afield.lang.rectsl.study
import afield.document.graph.Box
import afield.runtime.execution.BoxExecution
import afield.app.protocol.CompletionRequest
import afield.app.protocol.Execute
import afield.app.protocol.ExecuteSymbol
import afield.runtime.execution.ExecutionTrace
import afield.app.protocol.IsRunning
import afield.app.protocol.Message
import afield.app.protocol.Output
import afield.runtime.lifecycle.RunsAt
import afield.runtime.lifecycle.Runloop
import afield.app.server.SessionInformation
import afield.document.graph.autorun
import afield.runtime.execution.configureOutputProxies
import afield.runtime.execution.dispatchExecutionOutput
import afield.runtime.lifecycle.early
import afield.graphics.vulkan.Renderer
import afield.graphics.render.Mesh
import afield.graphics.render.Module
import afield.graphics.render.ModulePointer
import afield.graphics.render.parameters
import afield.graphics.render.pointer
import afield.graphics.vulkan.targets.RenderToTexture
import org.lwjgl.vulkan.VK10.VK_FILTER_NEAREST
import org.lwjgl.vulkan.VK10.VK_FORMAT_R8G8B8A8_UNORM
import afield.runtime.lifecycle.inMain
import afield.document.graph.isTrue
import afield.app.notifications.notify
import afield.app.diagnostics.logging.lprintln
import afield.app.protocol.payload
import afield.runtime.lifecycle.newRoot
import afield.runtime.lifecycle.plugins
import afield.lang.rectsl.RectSLProblem
import afield.lang.rectsl.SceneProgram
import afield.lang.rectsl.writeTo
import afield.runtime.MainLanguageRuntime
import afield.runtime.TopologyWatcher
import afield.runtime.applyRuntimeToggle
import afield.runtime.isMainLanguage
import afield.runtime.registerMainLanguageRuntime
import afield.app.protocol.send
import afield.app.commands.Action
import afield.study.source.BoxStudies
import afield.app.commands.actions
import afield.runtime.languages.LanguageRoots
import afield.runtime.execution.begin
import afield.app.server.thisSession
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
RectSL as a main language in Field: running a scene box compiles its rectSL, draws it live in the
window above it, and checks it in to the box's study.
RectSLPrograms holds the per-box studies, RectSLLive keeps a box's window following its edits,
RectSLExecution is the box execution, and the install... functions register the language, its line
notes, its topology watcher and its scene action.
object RectSLPrograms
RectSL as one of Field's main languages, and the home of every RectSL scene's study.
Mark a box "Set Main Language: RectSL" and its children are RectSL scenes: each edits its rectSL
property, and running it
- compiles it and puts every problem on the line it belongs to, as you type;
- draws it, live and looping, in the window above it — the first box up the parent chain that holds
a running
renderer, usually the root itself, which opens one of its own when it holds no Kotlin (LanguageRoots) — following every edit that compiles until stopped; - checks the scene in as a revision of the box's study (under
studies/<box id>/in the document) and shows the study's card in the box's output.
The study tools (installRectSLStudyTools) are offered to any AI4 conversation opened on a box's
rectSL property and work on that box, so a scene needs no tools box, and two scenes never share a study.
object RectSLPrograms {
const val PROPERTY = "rectSL"
The property a RectSL scene lives in.
const val CHANNEL = "study"
The output channel a scene's study card is shown in.
const val LANGUAGE = "rectsl"
The main-language id, as mainLanguage stores it.
val WINDOW = afield.app.protocol.Rect(80.0, 80.0, 960.0, 540.0)
Where the window a RectSL root opens for itself goes, and its size: a 16:9 wall at 960 by 540.
fun sessionOf(box: Box): SessionInformation? = BoxStudies.documentOf(box)
The open document box belongs to, or null.
fun rendererFor(box: Box): Renderer? = BoxStudies.rendererAbove(box)
The running renderer a scene draws in; see BoxStudies.rendererAbove.
The lock a box's study and card are made under — not the box's own monitor. Making a study resumes
it, which renders probe frames on the window's GPU and so waits for a frame the main thread draws;
the main thread, finishing a run's verdict, takes the box's monitor (ExecutionReports). Holding
that monitor here deadlocked the two: the scene's run never replied, and Field stopped drawing.
private val creationLocks = java.util.WeakHashMap<Box, Any>()
private fun creationLock(box: Box): Any = synchronized(creationLocks) { creationLocks.getOrPut(box) { Any() } }
fun study(box: Box): RectSLStudy
box's study, created on first use; publishing writes the box's rectSL on the main thread.
fun study(box: Box): RectSLStudy {
(box.getPropertyRaw("_rectSLStudy", null) as? RectSLStudy)?.let { return it }
synchronized(creationLock(box)) {
(box.getPropertyRaw("_rectSLStudy", null) as? RectSLStudy)?.let { return it }
val study = RectSLStudy(BoxStudies.directory(box), { box.getPropertyRaw(PROPERTY, "") as? String ?: "" }, BoxStudies.replacer(box, PROPERTY))
study.rasterizer = RectSLGpuRasterizer({ rendererFor(box) })
study.editorLine = { BoxStudies.editorLine(box, PROPERTY) }
study.resume()
box.properties["_rectSLStudy"] = study
return study
}
}
fun readyStudy(box: Box): RectSLStudy
study, with a first revision checked in if it has none. Draws probe frames, so call it off the main thread.
fun readyStudy(box: Box): RectSLStudy {
val study = study(box)
if (study.currentId() == afield.study.source.SourceStudy.EMPTY) study.checkIn("the starting scene")
viewer(box)
return study
}
fun viewer(box: Box): RectSLStudyViewer
The card showing box's study, created (and shown) on first use.
fun viewer(box: Box): RectSLStudyViewer {
(box.getPropertyRaw("_rectSLViewer", null) as? RectSLStudyViewer)?.let { return it }
synchronized(creationLock(box)) {
(box.getPropertyRaw("_rectSLViewer", null) as? RectSLStudyViewer)?.let { return it }
val viewer = RectSLStudyViewer(study(box), box, CHANNEL)
box.properties["_rectSLViewer"] = viewer
viewer.refreshLater()
return viewer
}
}
fun live(box: Box): RectSLLive
box's binding to the window above it.
fun live(box: Box): RectSLLive =
box.getPropertyRaw("_rectSLLive", null) as? RectSLLive ?: synchronized(box) {
box.getPropertyRaw("_rectSLLive", null) as? RectSLLive ?: RectSLLive(box).also { box.properties["_rectSLLive"] = it }
}
fun run(box: Box)
Run box as the editor's run does: draw it, follow its edits, and check it in.
fun run(box: Box) = box.ke.executeAll(box, Execute(box.id, "run", 1, box.getPropertyRaw(PROPERTY, "") as? String ?: "", false, property = PROPERTY)) {}
fun isScene(box: Box, property: String): Boolean = property == PROPERTY
Whether a conversation focused on box's property is about a RectSL scene.
internal val background
Check-ins draw probe frames, so they run here rather than on the main thread or a socket.
internal val background = Executors.newSingleThreadExecutor { r -> Thread(r, "rectsl-programs").apply { isDaemon = true } }
}
class RectSLLive internal constructor(private val box: Box)
A RectSL scene drawn live in a window, looping over its duration: the lowered rectangles through RectSLStudyShader's shaders into an 8-bit target the size of the window, shown by a fullscreen pass that hands the swapchain exactly those bytes (see RectSLStudyShader for why not straight into the window) — the whole wall fitted in, with black around it.
A new scene whose materials compile to the same fragment shader as the one drawing only swaps the
scene the mesh is filled from — nothing is rebuilt, so typing a number moves a rectangle on the next
frame. A new material builds a new pipeline (a Module compiles in its constructor, so a failure is
known at once), and a resized window a new target; what they replace is retired through the
renderer's deferred queue, because a frame in flight may still use it. All of it on the main thread.
The card can hold the clock at one time (hold), restart it, and show a scene other than the box's while its sliders move (preview).
class RectSLLive internal constructor(private val box: Box) {
private val name = "rectsl-live-${box.id}"
private var renderer: Renderer? = null
private var target: RenderToTexture? = null
private var composite: ModulePointer? = null
private var pointer: ModulePointer? = null
private var builtFragment: String? = null
private var builtExtent: Pair<Int, Int>? = null
private var shown: String? = null
@Volatile private var scene: SceneProgram? = null
@Volatile private var started = System.nanoTime()
@Volatile private var frameTime = 0.0
var held: Double?
Seconds to hold the window at, or null to play.
@Volatile
var held: Double? = null
private set
var preview: RectSLDrawable?
A scene the card is previewing in place of the box's, or null.
@Volatile
var preview: RectSLDrawable? = null
var pinnedLine: Int?
A line the card pinned the window to: it shows the scene as it stands after that line. Null for the whole scene.
@Volatile
var pinnedLine: Int? = null
var followCursor: Boolean
Whether the window shows the scene as it stands at the editor's line, rather than the whole of it.
@Volatile
var followCursor: Boolean = false
fun shownLine(cursor: Int?): Int? = pinnedLine ?: cursor?.takeIf { followCursor }
The line the window is showing the scene up to, given the editor's cursor; null for the whole scene.
var outlined: Set<String>
The rectangles to outline in the window: those the editor's line makes (see RectSLWindowLinks).
@Volatile
var outlined: Set<String> = emptySet()
var drawing: RectSLDrawable?
The scene the window is drawing, with the lines its rectangles came from, or null.
@Volatile
var drawing: RectSLDrawable? = null
private set
Takes the click listener off the window it was put on.
private var unlisten: (() -> Unit)? = null
val showing: String?
The source the window above the box is drawing, or null — also when that window closed, changed, or was resized since (so the next present rebuilds its target).
val showing: String?
get() = shown?.takeIf { renderer?.isClosed == false && RectSLPrograms.rendererFor(box) === renderer && extentOf(renderer!!) == builtExtent }
fun time(): Double
The time the window is drawing now, in the scene's seconds.
fun time(): Double {
held?.let { return it }
val duration = scene?.duration?.takeIf { it > 0 } ?: return 0.0
return ((System.nanoTime() - started) / 1e9) % duration
}
fun hold(seconds: Double?)
Hold the window at seconds, or play again from there with null.
fun hold(seconds: Double?) {
val now = time()
held = seconds
if (seconds == null) started = System.nanoTime() - (now * 1e9).toLong()
}
fun restart()
Play from the start.
fun restart() {
held = null
started = System.nanoTime()
}
fun present(good: RectSLDrawable?): String?
Show good — or the card's preview while there is one — if it is not what the window is drawing.
fun present(good: RectSLDrawable?): String? {
val wanted = preview ?: good ?: return null
return if (showing == wanted.source) null else show(wanted)
}
private fun extentOf(r: Renderer): Pair<Int, Int>? = runCatching {
r.windowContext(0).swapChain.swapChainExtent.let { it.width() to it.height() }
}.getOrNull()?.takeIf { it.first > 0 && it.second > 0 }
fun show(drawable: RectSLDrawable): String?
Draw drawable in the window above the box. Returns null when it is drawing, otherwise why not:
no window, or a shader the GPU refused (the previous scene keeps drawing).
body — 92 lines, from line 264
fun show(drawable: RectSLDrawable): String? = onMain {
val r = RectSLPrograms.rendererFor(box) ?: return@onMain "no window: this box has no running renderer above it"
if (r !== renderer) { hideNow(); renderer = r; listen(r) }
val extent = extentOf(r) ?: return@onMain "no window: the renderer has no window to draw in"
val fragment = RectSLStudyShader.fragment(drawable.materials)
val current = pointer
if (current != null && builtFragment == fragment && builtExtent == extent && current.current.valid && r.renderList.preTargets[name] === target) {
scene = drawable.scene
drawing = drawable
shown = drawable.source
return@onMain null
}
if (builtExtent != extent || target == null || composite == null) {
// A new target for a new window size, and the pass that shows it; the scene module is rebuilt below.
retire(r)
val t = RenderToTexture(r) {
width = extent.first
height = extent.second
format = VK_FORMAT_R8G8B8A8_UNORM
useDepth = false
colorSampled = true
clearColor = afield.math.linalg.vec(0f, 0f, 0f, 1f)
debugName = name
}
val sampler = t.defaultSampler ?: t.setDefaultSampler(anisotropyEnable = false, filter = VK_FILTER_NEAREST)
var error: String? = null
val shownBy = Module(parameters(r) {
this.name = "${this@RectSLLive.name}-composite"
vertex = RectSLStudyShader.COMPOSITE_VERTEX
this.fragment = RectSLStudyShader.COMPOSITE_FRAGMENT
blend = false
depthTest = false
textureResolver = { texture -> if (texture.name == "rectslScene") t to sampler else null }
errorHandler = { message, _ -> error = message }
}).pointer()
if (!shownBy.current.valid) {
r.cleanup.add(Runnable { dispose(shownBy); disposeTarget(t) })
return@onMain "the GPU refused the window pass: ${error ?: "unknown error"}"
}
val fullscreen = Mesh(r, shownBy)
fullscreen.resize(3, 0, shownBy)
shownBy.current.meshes["fullscreen"] = fullscreen
target = t
composite = shownBy
builtExtent = extent
}
val t = target!!
var error: String? = null
val fresh = Module(parameters(r) {
this.name = this@RectSLLive.name
vertex = RectSLStudyShader.VERTEX
this.fragment = fragment
blend = true
depthTest = false
primitiveType = triangles
+t
errorHandler = { message, _ -> error = message }
}).pointer()
if (!fresh.current.valid) {
r.cleanup.add(Runnable { dispose(fresh) })
return@onMain "the GPU refused the scene's shader: ${error ?: "unknown error"}"
}
scene = drawable.scene
val mesh = Mesh(r, fresh)
mesh.push.bind("worldToClip") { RectSLStudyShader.contain(scene ?: drawable.scene, extent.first, extent.second) }
mesh.push.bind("time") { frameTime.toFloat() }
mesh.push.bind("wallWidth") { (scene ?: drawable.scene).wall.width.toFloat() }
mesh.push.bind("wallHeight") { (scene ?: drawable.scene).wall.height.toFloat() }
// An outline a couple of the window's points wide, whatever the wall's units and the display's density.
val points = runCatching { r.window.size().w }.getOrNull()?.takeIf { it > 0 } ?: extent.first.toDouble()
mesh.update["rectsl-frame"] = sequence {
while (true) {
frameTime = time()
scene?.let {
val point = RectSLStudyShader.visible(it, extent.first, extent.second)[2] / points
RectSLStudyShader.mesh(it, frameTime, outlined, point).writeTo(mesh)
}
yield(Unit)
}
}
fresh.current.meshes["scene"] = mesh
val previous = pointer
t += name to fresh
r.renderList.preTargets[name] = t
r.renderList.modules["${name}-composite"] = composite!!
pointer = fresh
builtFragment = fragment
drawing = drawable
shown = drawable.source
previous?.takeIf { it !== fresh }?.let { old -> r.cleanup.add(Runnable { dispose(old) }) }
null
}
fun hide()
Stop drawing in the window. Nothing to hide costs nothing, and does not wait for the main thread.
fun hide() {
if (pointer == null && shown == null && target == null) return
onMain { hideNow() }
}
private fun hideNow() {
renderer?.let { retire(it) }
// Forget the window too, so the next show listens to it again.
renderer = null
unlisten?.invoke(); unlisten = null
shown = null
drawing = null
outlined = emptySet()
}
A click in r's window puts the editor on the line that made the rectangle under it (RectSLWindowLinks); the listener goes when the scene leaves the window.
private fun listen(r: Renderer) {
unlisten?.invoke()
val window = runCatching { r.window }.getOrNull() ?: return
unlisten = window.addMouseButtonListener { button, action, _ ->
if (button != org.lwjgl.glfw.GLFW.GLFW_MOUSE_BUTTON_LEFT || action != org.lwjgl.glfw.GLFW.GLFW_PRESS) return@addMouseButtonListener
runCatching {
val size = window.size()
val at = window.cursor
pick(at.x.toDouble() / size.w, at.y.toDouble() / size.h, size.w / size.h)
}.onFailure { lprintln("warning,rectsl", "picking a rectangle in the window failed: $it") }
}
}
fun pick(u: Double, v: Double, aspect: Double): Int?
What a click at (u, v) of the window does — u across from the left, v down from the top, both
0..1, in a window aspect wide for each unit high: find the topmost rectangle under it now and put the
editor on the line that made it. Returns that line, or null when the click missed.
fun pick(u: Double, v: Double, aspect: Double): Int? {
val d = drawing ?: return null
if (u !in 0.0..1.0 || v !in 0.0..1.0) return null
val point = RectSLStudyShader.wallPoint(d.scene, (aspect * 1000).toInt(), 1000, u, v)
val hit = RectSLWindowLinks.hit(d.scene, time(), point.x, point.y) ?: return null
val line = d.origins[hit.id]?.line ?: return null
RectSLWindowLinks.reveal(box, line)
return line
}
Take the target, its pass and the scene module out of r's frame and queue them for disposal.
private fun retire(r: Renderer) {
val t = target; val c = composite; val m = pointer
if (t != null && r.renderList.preTargets[name] === t) r.renderList.preTargets.remove(name)
if (c != null && r.renderList.modules["${name}-composite"] === c) r.renderList.modules.remove("${name}-composite")
if (!r.isClosed) r.cleanup.add(Runnable {
m?.let(::dispose)
c?.let(::dispose)
t?.let(::disposeTarget)
})
target = null; composite = null; pointer = null
builtFragment = null; builtExtent = null
}
private fun dispose(module: ModulePointer) {
runCatching {
module.current.meshes.values.toSet().forEach { mesh ->
(mesh.v.values + mesh.vn.values).toSet().forEach { it.cleanup() }
mesh.t?.cleanup()
}
module.current.meshes.clear()
module.current.cleanup()
}
}
private fun disposeTarget(rtt: RenderToTexture) {
runCatching {
rtt.modules.clear()
rtt.defaultSampler?.cleanup()
rtt.imageViewArray.map { it.imageView }.distinctBy { it.vkImageView }.forEach { it.cleanup() }
rtt.colorImageRefs.filterNotNull().distinctBy { it.vkImage }.forEach { it.cleanup() }
rtt.commandBuffers.forEach { it.cleanup() }
rtt.fences.forEach { it.cleanup() }
}
}
private fun <T> onMain(block: () -> T): T =
if (Runloop.isMainThread) block() else runBlocking { withContext(Runloop.runLoop.main) { block() } }
}
class RectSLExecution(val inside: Box) : BoxExecution
How a child of a RectSL root runs: see RectSLPrograms. Run and launch both draw the scene and follow
its edits until stopped; run also checks it in as a revision. Compiling happens off the main thread
(a {{ … }} escape compiles Kotlin); drawing and reporting on it.
override val languageName: String get() = RectSLPrograms.LANGUAGE
private fun source(execute: Execute, code: String = execute.code): String =
if (execute.property == RectSLPrograms.PROPERTY) code
else inside.getPropertyRaw(RectSLPrograms.PROPERTY, "") as? String ?: ""
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit) = start(execute, source(execute), "default", reply, checkIn
override fun executeFragment(inside: Box, execute: Execute, reply: (Message) -> Unit) = start(execute, source(execute), "default", reply, checkIn = true)
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) = start(execute, source(execute), "default", reply, checkIn
override fun executeAll(inside: Box, execute: Execute, reply: (Message) -> Unit) = start(execute, source(execute), "default", reply, checkIn = true)
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit) =
override fun launchAll(inside: Box, execute: Execute, code: String, strand: String, reply: (Message) -> Unit) =
start(execute, source(execute, code), strand, reply, checkIn = false)
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit) =
override fun executeSymbol(inside: Box, executeSymbol: ExecuteSymbol, reply: (Message) -> Unit) =
Execute(executeSymbol.uid, "run", 1, "", false).let { start(it, source(it), "default", reply, checkIn = true) }
override fun stop(inside: Box)
override fun stop(inside: Box) {
inside.runGeneration.keys.toList().forEach { inside.runGeneration[it] = (inside.runGeneration[it] ?: 0) + 1 }
RectSLPrograms.live(inside).hide()
}
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit) =
The evaluator's vocabulary and the scene's own definitions (afield.lang.rectsl.RectSLVocabulary.complete).
body — 75 lines, from line 473
override fun complete(inside: Box, completionRequest: CompletionRequest, reply: (Message) -> Unit) =
reply(Message("complete").payload(afield.lang.rectsl.RectSLVocabulary.complete(completionRequest.allCode, completionRequest.position)))
@OptIn(DelicateCoroutinesApi::class)
private fun start(execute: Execute, first: String, strand: String, reply: (Message) -> Unit, checkIn: Boolean) {
val originSession = thisSession.get()
val trace = ExecutionTrace.forEngine(inside, execute, if (checkIn) "run" else "launch", reply)
inMain {
configureOutputProxies(inside, trace.reply, originSession)
// Starting a scene is what opens its window, when its root has none (see LanguageRoots).
val noWindow = LanguageRoots.ensure(inside, RectSLPrograms.LANGUAGE, "RectSL", RectSLPrograms.WINDOW)
val generation = inside.runGeneration.getOrPut(strand) { -1 } + 1
inside.runGeneration[strand] = generation
// Running as Field counts it, so what stops a box's dependents — a window's teardown — stops this scene too.
inside.currentRunningStates[strand] = afield.document.graph.RunningState(generation, RectSLPrograms.LANGUAGE)
send(Message("is-running").payload(IsRunning(execute.uid, true, generation, strand)))
GlobalScope.launch(Dispatchers.Default) {
val live = RectSLPrograms.live(inside)
val startedAt = System.currentTimeMillis()
var toldNoWindow = noWindow == null
try {
var lastGood = compile(first, trace.reply, originSession)
val refused = live.present(lastGood)?.takeIf { !it.startsWith("no window") }
val ok = lastGood != null && refused == null
withContext(Runloop.runLoop.main) {
refused?.let { inside.displayError(it, RectSLPrograms.PROPERTY, 1) }
if (ok) trace.finish(true) else trace.finish(false, "the scene has problems; the window keeps the last scene that compiled")
}
if (refused != null) lastGood = null
if (checkIn && ok) checkInLater()
else RectSLPrograms.background.execute { runCatching { RectSLPrograms.viewer(inside).refreshLater() } }
// Follow the box: report each new edit's problems once, and keep the last edit that
// compiled in the window — including a window that opens, or is relaunched, later.
var last = first
var changedAt = 0L
while (inside.runGeneration[strand] == generation) {
delay(FOLLOW_MS)
if (inside.runGeneration[strand] != generation) break
val (now, cursor) = withContext(Runloop.runLoop.main) {
(inside.getPropertyRaw(RectSLPrograms.PROPERTY, "") as? String ?: "") to RectSLWindowLinks.cursorLine(inside)
}
// The editor's line, outlined in the window.
live.outlined = live.drawing?.let { d -> cursor?.let { RectSLWindowLinks.idsForLine(d, it) } }.orEmpty()
if (now != last) {
last = now
changedAt = System.currentTimeMillis()
compile(now, trace.reply, originSession)?.let { lastGood = it }
}
if (changedAt > 0 && System.currentTimeMillis() - changedAt > SETTLE_MS) {
changedAt = 0
(inside.getPropertyRaw("_rectSLViewer", null) as? RectSLStudyViewer)?.refreshLater()
}
// Pinned to a line, or following the editor's: the scene as it stands after that line.
val partial = live.shownLine(cursor)?.let { l -> RectSLPrograms.study(inside).drawableThrough(now, l) }
live.present(partial ?: lastGood)?.takeIf { !it.startsWith("no window") }?.let {
withContext(Runloop.runLoop.main) { inside.displayError(it, RectSLPrograms.PROPERTY, 1) }
lastGood = null
}
// A card drawn before the window was up says the CPU drew it; redraw it once it is.
if (live.showing != null) (inside.getPropertyRaw("_rectSLViewer", null) as? RectSLStudyViewer)?.redrawIfDrawnWithoutWindow()
// Say why there is no window — after a moment, since a root's own Kotlin may be opening one.
else if (!toldNoWindow && System.currentTimeMillis() - startedAt > NO_WINDOW_GRACE_MS) {
toldNoWindow = true
notify("'${inside.name}': $noWindow", RectSLPrograms.LANGUAGE)
}
}
} catch (e: Exception) {
lprintln("warning,rectsl", "running '${inside.name}' failed: $e")
} finally {
if (inside.currentRunningStates[strand]?.watermark == generation) inside.currentRunningStates.remove(strand)
send(Message("is-running").payload(IsRunning(execute.uid, false, generation, strand)))
}
}
}
}
Compile source and put its problems on its lines, replacing the last report; the drawable if it has no errors.
source — 43 lines, from line 550
private suspend fun compile(source: String, reply: (Message) -> Unit, originSession: io.ktor.websocket.WebSocketSession?): RectSLDrawable? {
val (drawable, problems) = try {
val compiled = RectSLStudy.compile(source)
val program = compiled.program
(if (compiled.ok && program != null) RectSLDrawable(source, program, compiled.materials, compiled.origins) else null) to compiled.problems
} catch (e: Exception) {
null to listOf(RectSLProblem(e.message ?: "$e", null, null))
}
withContext(Runloop.runLoop.main) {
dispatchExecutionOutput(inside, reply,
Output(uid = inside.id, line = -1, kind = "err", property = RectSLPrograms.PROPERTY, html = "", frame = Runloop.runLoop.frame.toInt()),
originSession = originSession)
problems.forEach { p -> inside.displayError((if (p.error) "" else "warning: ") + p.message, RectSLPrograms.PROPERTY, p.line ?: 1) }
}
return drawable
}
private fun checkInLater() {
RectSLPrograms.background.execute {
try {
val study = RectSLPrograms.readyStudy(inside)
// Running the box shows its card as it stands: a new revision redraws it, and so does this.
if (study.revisions.liveState()["dirty"] == true) study.checkIn("run by hand")
else RectSLPrograms.viewer(inside).refreshLater()
} catch (e: afield.study.source.StudyRejection) {
inMain { inside.displayError("not checked in: ${e.message}", RectSLPrograms.PROPERTY, 1) }
} catch (e: Exception) {
lprintln("warning,rectsl", "checking in '${inside.name}' failed: $e")
}
}
}
private companion object {
/** How often a running scene looks for a new edit to draw. */
const val FOLLOW_MS = 60L
/** How long an edit must stand before the card is rebuilt to show it. */
const val SETTLE_MS = 1200L
/** How long a started scene waits for a window before saying why it has none. */
const val NO_WINDOW_GRACE_MS = 3000L
}
}
How long an autorun scene waits after becoming RectSL before starting, so its window can open.
private const val AUTOSTART_DELAY_MS = 800L
fun installRectSLLanguage()
Register RectSL as a main language, so "Set Main Language: RectSL" is offered on a root box.
@RunsAt(early)
fun installRectSLLanguage() {
registerMainLanguageRuntime(
MainLanguageRuntime(
id = RectSLPrograms.LANGUAGE,
label = "RectSL",
runtimeClass = RectSLExecution::class,
setLegacyRoot = { _, _ -> },
applyTopology = ::rectSLTopologyNeedsUpdating,
),
)
}
fun installRectSLLineNotes()
A RectSL scene's lines, for the editor's completion popup and cursor notes: the scene built up to each,
at the time its window is showing (RectSLStudy.lineNote).
@RunsAt(plugins)
fun installRectSLLineNotes() {
afield.ui.output.LineNotes.register(RectSLPrograms.PROPERTY, { it.ke is RectSLExecution }) { box, source, line ->
val live = RectSLPrograms.live(box)
RectSLPrograms.study(box).lineNote(source, line, if (live.showing != null) live.time() else 0.0)
}
}
fun installRectSLLanguageWatcher()
Re-apply rectSLTopologyNeedsUpdating whenever the graph changes.
@RunsAt(plugins)
fun installRectSLLanguageWatcher() {
TopologyWatcher(RectSLPrograms.LANGUAGE) { rectSLTopologyNeedsUpdating(it) }.start()
}
const val RECTSL_STARTER
The scene a new RectSL box starts with: a wall, a background, and one rectangle to change.
const val RECTSL_STARTER = """# A RectSL scene. The window above draws it, looping; edit a number and watch it move.
->wall 16 9 meters
->duration 6
->fps 30
->background 0x101014
"square" rect
0 0 ->xy
2 ->square
0xf0b040 solid ->fill
'( 0 0 6 6.28318 ) ->rot
;
"""
fun installRectSLSceneAction(root: Box)
"Make a RectSL Scene": the selected box becomes a scene beneath a RectSL root — a new one just above it, if it has none, which opens a window of its own — and starts playing. A selected root gets a new scene beneath it.
@RunsAt(newRoot)
fun installRectSLSceneAction(root: Box) {
root.actions["make-rectsl-scene"] = Action(
desc = "Make a RectSL Scene",
detail = "The selected box becomes a RectSL scene, playing live in a window of its own. Its AI4 chat gets tools that check, draw, compare and version it.",
) { selection ->
inMain {
LanguageRoots.adopt(selection, RectSLPrograms.LANGUAGE, "RectSL window", "RectSL scene", RectSLPrograms.PROPERTY, RECTSL_STARTER).forEach { scene ->
scene.properties["_rectSLAutostarted"] = true
RectSLPrograms.run(scene)
notify("'${scene.name}' is a RectSL scene: edit it and watch the window, or describe what you want in its AI4 chat", RectSLPrograms.LANGUAGE)
}
}
}
}
fun rectSLTopologyNeedsUpdating(values: Collection<Box>)
Give every child of a RectSL root a RectSLExecution that edits rectSL, and hand a child back to
Kotlin when no RectSL root claims it any more.
fun rectSLTopologyNeedsUpdating(values: Collection<Box>) {
applyRuntimeToggle(
boxes = values,
isRoot = { it.isMainLanguage(RectSLPrograms.LANGUAGE) },
runtimeClass = RectSLExecution::class,
newExecution = { child -> RectSLExecution(child) },
language = "RectSL",
customization = "",
notifyCategory = RectSLPrograms.LANGUAGE,
onAttach = { child ->
if ((child.getPropertyRaw("default_property", null) as? String).isNullOrBlank()) {
child.setPropertyRaw("default_property", RectSLPrograms.PROPERTY, doResync = true)
}
if (child.getPropertyRaw(RectSLPrograms.PROPERTY, null) == null) child.setPropertyRaw(RectSLPrograms.PROPERTY, RECTSL_STARTER, doResync = true)
// Field's autorun pass may run before this box became RectSL, so an autorun scene starts
// itself — once: attach is repeated on every graph change.
if (child.autorun.isTrue() && child.getPropertyRaw("_rectSLAutostarted", null) != true) {
child.properties["_rectSLAutostarted"] = true
// Not over a run that started meanwhile — a run is what gives a box its language at once.
inMain { delay(AUTOSTART_DELAY_MS); if (child.currentRunningStates.isEmpty()) child.begin() }
}
},
onDetach = { child -> runCatching { RectSLPrograms.live(child).hide() } },
)
}