package org.openrndr.extra.olive import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import org.openrndr.Extension import org.openrndr.Program import org.openrndr.draw.Session import org.openrndr.events.Event import org.openrndr.exceptions.stackRootClassName import org.openrndr.extra.kotlinparser.extractProgram import org.openrndr.launch import org.openrndr.extra.filewatcher.watchFile import java.io.File private val logger = KotlinLogging.logger {} private fun Event.saveListeners(store: MutableMap, List<(Any) -> Unit>>) { @Suppress("UNCHECKED_CAST") store[this] = listeners.map { it } as List<(Any) -> Unit> } private fun Event.restoreListeners(store: Map, List<(Any) -> Unit>>) { listeners.retainAll(store[this] ?: emptyList()) } enum class OliveScriptHost { JSR223, JSR223_REUSE, KOTLIN_SCRIPT } data class ScriptLoadedEvent(val scriptFile: String) enum class ScriptMode { KOTLIN_SCRIPT, OLIVE_PROGRAM } class Olive

(val resources: Resources? = null, private var scriptMode: ScriptMode = ScriptMode.KOTLIN_SCRIPT) : Extension { override var enabled: Boolean = true var session: Session? = null var scriptHost = OliveScriptHost.JSR223_REUSE val scriptLoaded = Event() internal var scriptChange: (String) -> Unit = {} var script = when (scriptMode) { ScriptMode.KOTLIN_SCRIPT -> "src/main/kotlin/${stackRootClassName().split(".").last()}.kts" else -> "src/main/kotlin/${stackRootClassName().split(".").last()}.kt" } set(value) { field = value scriptChange(value) } /** * reloads the active script */ fun reload() { // watcher?.triggerChange() } class ScriptWatcher private var watcherRequestStopEvent = Event() private var watcher: (() -> Unit)? = null @OptIn(DelicateCoroutinesApi::class) override fun setup(program: Program) { System.setProperty("idea.io.use.fallback", "true") System.setProperty("org.openrndr.ignoreShadeStyleErrors", "true") val store = mutableMapOf, List<(Any) -> Unit>>() val originalExtensions = program.extensions.map { it } val trackedListeners = listOf>(program.mouse.buttonDown, program.mouse.buttonUp, program.mouse.dragged, program.mouse.moved, program.mouse.scrolled, program.keyboard.keyUp, program.keyboard.keyDown, program.keyboard.keyRepeat, program.keyboard.character, program.window.drop, program.window.focused, program.window.minimized, program.window.moved, program.window.sized, program.window.unfocused, program.requestAssets, program.produceAssets ) trackedListeners.forEach { it.saveListeners(store) } val originalAssetMetadata = program.assetMetadata val originalAssetProperties = program.assetProperties.toMutableMap() fun setupScript(scriptFile: String) { if (watcher != null) { logger.info { "requesting watcher stop" } watcherRequestStopEvent.trigger(Unit) } else { logger.info { "no existing watcher" } } val f = File(scriptFile) if (!f.exists()) { f.parentFile.mkdirs() var className = program.javaClass.name if (className.contains("$")) className = "Program" f.writeText(""" @file:Suppress("UNUSED_LAMBDA_EXPRESSION") import org.openrndr.Program import org.openrndr.draw.* { program: $className -> program.apply { extend { } } } """.trimIndent()) } val jsr233ObjectLoader = if (scriptHost == OliveScriptHost.JSR223_REUSE) ScriptObjectLoader() else null watcher = watchFile(File(script), requestStopEvent = watcherRequestStopEvent) { try { logger.info { "change detected, reloading script" } val scriptContents = when (scriptMode) { ScriptMode.KOTLIN_SCRIPT -> it.readText() ScriptMode.OLIVE_PROGRAM -> { val source = it.readText() val programSource = extractProgram(source, programIdentifier = "oliveProgram") generateScript(programSource) } } val futureFunc = GlobalScope.async { val start = System.currentTimeMillis() val loadedFunction = when (scriptHost) { OliveScriptHost.JSR223_REUSE -> loadFromScriptContents(scriptContents, jsr233ObjectLoader!!) OliveScriptHost.JSR223 -> loadFromScriptContents(scriptContents) OliveScriptHost.KOTLIN_SCRIPT -> loadFromScriptContentsKSH Unit>(scriptContents) } val end = System.currentTimeMillis() logger.info { "loading script took ${end - start}ms" } loadedFunction } program.launch { val func = futureFunc.await() program.extensions.forEach {extension -> extension.shutdown(program) } program.extensions.clear() program.extensions.addAll(originalExtensions) program.assetMetadata = originalAssetMetadata program.assetProperties = originalAssetProperties trackedListeners.forEach { l -> l.restoreListeners(store) } session?.end() session = Session.root.fork() @Suppress("UNCHECKED_CAST") func(program as P) scriptLoaded.trigger(ScriptLoadedEvent(scriptFile)) Unit } Unit } catch (e: Throwable) { e.printStackTrace() } } } setupScript(script) scriptChange = ::setupScript if (resources != null) { val srcPath = "src/main/resources" var src = File(srcPath) resources.watch(src) { file -> val dest = "build/resources/main" val filePath = file.path.split(Regex(srcPath), 2).getOrNull(1) val destFile = File("$dest/${filePath}").absoluteFile watchFile(file) { if (resources[file]!! && filePath != null) { file.copyTo(destFile, overwrite = true) reload() } else { resources[file] = true } } } } } }