Upgrade to OPENRNDR 0.4 snapshot
This commit is contained in:
146
orx-jvm/orx-olive/README.md
Normal file
146
orx-jvm/orx-olive/README.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# orx-olive
|
||||
|
||||
Provides live coding functionality: updates a running OPENRNDR program when you save your changes.
|
||||
|
||||
## usage
|
||||
|
||||
make sure that you add the following to your list of dependencies (next to orx-olive)
|
||||
```
|
||||
implementation "org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.3.31"
|
||||
```
|
||||
|
||||
Then a simple live setup can created as follows:
|
||||
|
||||
```kotlin
|
||||
import org.openrndr.Program
|
||||
import org.openrndr.application
|
||||
import org.openrndr.extra.olive.Olive
|
||||
|
||||
fun main() = application {
|
||||
configure {
|
||||
width = 768
|
||||
height = 576
|
||||
}
|
||||
program {
|
||||
extend(Olive<Program>())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The extension will create a template script for you in `src/main/kotlin/live.kts`. You can
|
||||
edit this to see how the program updates automatically.
|
||||
|
||||
## Shade style errors
|
||||
|
||||
Recent versions of `orx-olive` automatically set the `org.openrndr.ignoreShadeStyleErrors` property which
|
||||
makes OPENRNDR ignore errors in the shade style and return the default shader. To get this behaviour in
|
||||
older versions add `-Dorg.openrndr.ignoreShadeStyleErrors=true` to the JVM arguments.
|
||||
|
||||
## Reloadable State
|
||||
|
||||
Along with the extension comes a mechanism that allows state to be reloaded from a store on script reload.
|
||||
This functionality is offered by the `Reloadable` class.
|
||||
|
||||
An example `live.kts` in which the reloadable state is used:
|
||||
```kotlin
|
||||
@file:Suppress("UNUSED_LAMBDA_EXPRESSION")
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.*
|
||||
|
||||
{ program: PersistentProgram ->
|
||||
program.apply {
|
||||
val a = object : Reloadable() {
|
||||
var x : Double = 0.0
|
||||
}
|
||||
a.reload()
|
||||
|
||||
extend {
|
||||
// do something with a.x here
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The Reloadable store can be cleared using the `clearReloadables` function.
|
||||
|
||||
### Reloadable GPU resources
|
||||
|
||||
To store GPU resources or objects that use GPU resources (a.o. `ColorBuffer`, `VertexBuffer`, `Shader`, `BufferTexture`) in a `Reloadable` object one uses OPENRNDR's
|
||||
`persistent {}` builder function.
|
||||
|
||||
```!kotlin
|
||||
@file:Suppress("UNUSED_LAMBDA_EXPRESSION")
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.*
|
||||
|
||||
{ program: PersistentProgram ->
|
||||
program.apply {
|
||||
val a = object : Reloadable() {
|
||||
var image = persistent { loadImage("data/images/pm5544.png" ) }
|
||||
}
|
||||
a.reload()
|
||||
|
||||
extend {
|
||||
drawer.image(a.image)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
Keep in mind that `Reloadable` should only be used for singleton classes.
|
||||
|
||||
## Persistent Data
|
||||
Sometimes you want to keep parts of your application persistent. In the following example
|
||||
we show how you can prepare the host program to contain a persistent camera device.
|
||||
|
||||
```kotlin
|
||||
import org.openrndr.Program
|
||||
import org.openrndr.application
|
||||
|
||||
class PersistentProgram: Program() {
|
||||
lateinit var camera: FFMPEGVideoPlayer
|
||||
}
|
||||
|
||||
fun main() = application{
|
||||
program(PersistentProgram()) {
|
||||
camera = FFMPEGVideoPlayer.fromDevice()
|
||||
camera.start()
|
||||
|
||||
extend(Olive<PersistentProgram>()) {
|
||||
script = "src/main/PersistentCamera.Kt"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The live script `src/main/PersistentCamera.kts` then looks like this:
|
||||
|
||||
```kotlin
|
||||
@file:Suppress("UNUSED_LAMBDA_EXPRESSION")
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.*
|
||||
|
||||
{ program: PersistentProgram ->
|
||||
program.apply {
|
||||
extend {
|
||||
camera.next()
|
||||
drawer.drawStyle.colorMatrix = tint(ColorRGBa.GREEN) * grayscale(0.0, 0.0, 1.0)
|
||||
camera.draw(drawer)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
<!-- __demos__ -->
|
||||
## Demos
|
||||
### DemoOlive01
|
||||
[source code](src/demo/kotlin/DemoOlive01.kt)
|
||||
|
||||

|
||||
|
||||
### DemoOliveScriptless01
|
||||
[source code](src/demo/kotlin/DemoOliveScriptless01.kt)
|
||||
|
||||

|
||||
27
orx-jvm/orx-olive/build.gradle
Normal file
27
orx-jvm/orx-olive/build.gradle
Normal file
@@ -0,0 +1,27 @@
|
||||
sourceSets {
|
||||
demo {
|
||||
java {
|
||||
srcDirs = ["src/demo/kotlin"]
|
||||
compileClasspath += main.getCompileClasspath()
|
||||
runtimeClasspath += main.getRuntimeClasspath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":orx-file-watcher")
|
||||
implementation project(":orx-kotlin-parser")
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-scripting-jvm:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-scripting-jvm-host:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"
|
||||
implementation "org.jetbrains.kotlin:kotlin-scripting-jsr223:$kotlinVersion"
|
||||
|
||||
|
||||
demoImplementation(project(":orx-camera"))
|
||||
demoImplementation("org.openrndr:openrndr-application:$openrndrVersion")
|
||||
demoImplementation("org.openrndr:openrndr-extensions:$openrndrVersion")
|
||||
demoRuntimeOnly("org.openrndr:openrndr-gl3:$openrndrVersion")
|
||||
demoRuntimeOnly("org.openrndr:openrndr-gl3-natives-$openrndrOS:$openrndrVersion")
|
||||
demoImplementation(sourceSets.getByName("main").output)
|
||||
}
|
||||
BIN
orx-jvm/orx-olive/images/DemoOlive01Kt.png
Normal file
BIN
orx-jvm/orx-olive/images/DemoOlive01Kt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
33
orx-jvm/orx-olive/src/demo/kotlin/DemoOlive01.kt
Normal file
33
orx-jvm/orx-olive/src/demo/kotlin/DemoOlive01.kt
Normal file
@@ -0,0 +1,33 @@
|
||||
import org.openrndr.Extension
|
||||
import org.openrndr.Program
|
||||
import org.openrndr.application
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extra.olive.Olive
|
||||
|
||||
fun main() = application {
|
||||
configure {
|
||||
width = 768
|
||||
height = 576
|
||||
}
|
||||
program {
|
||||
|
||||
extend(Olive<Program>()) {
|
||||
script = "orx-olive/src/demo/kotlin/demo-olive-01.kts"
|
||||
// -- this block is for automation purposes only
|
||||
if (System.getProperty("takeScreenshot") == "true") {
|
||||
scriptLoaded.listen {
|
||||
// -- this is a bit of hack, we need to push the screenshot extension in front of the olive one
|
||||
fun <T : Extension> Program.extendHead(extension: T, configure: T.() -> Unit): T {
|
||||
extensions.add(0, extension)
|
||||
extension.configure()
|
||||
extension.setup(this)
|
||||
return extension
|
||||
}
|
||||
extendHead(SingleScreenshot()) {
|
||||
this.outputFile = System.getProperty("screenshotPath")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
orx-jvm/orx-olive/src/demo/kotlin/DemoOliveScriptless01.kt
Normal file
43
orx-jvm/orx-olive/src/demo/kotlin/DemoOliveScriptless01.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
import org.openrndr.Extension
|
||||
import org.openrndr.application
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.extensions.SingleScreenshot
|
||||
import org.openrndr.extra.olive.OliveScriptHost
|
||||
import org.openrndr.extra.olive.oliveProgram
|
||||
import kotlin.math.cos
|
||||
|
||||
fun main() {
|
||||
application {
|
||||
configure {
|
||||
width = 1280
|
||||
height = 720
|
||||
}
|
||||
oliveProgram(scriptHost = OliveScriptHost.JSR223) {
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.GRAY)
|
||||
drawer.fill = ColorRGBa.WHITE
|
||||
for (i in 0 until 100) {
|
||||
drawer.circle(
|
||||
width / 2.0 + cos(seconds + i) * 320.0,
|
||||
i * 7.2,
|
||||
cos(i + seconds * 0.5) * 20.0 + 20.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
// -- this is only needed for the automated screenshots
|
||||
.olive.scriptLoaded.listen {
|
||||
if (System.getProperty("takeScreenshot") == "true") {
|
||||
// -- this is a bit of hack, we need to push the screenshot extension in front of the olive one
|
||||
fun <T : Extension> extendHead(extension: T, configure: T.() -> Unit): T {
|
||||
program.extensions.add(0, extension)
|
||||
extension.configure()
|
||||
extension.setup(program)
|
||||
return extension
|
||||
}
|
||||
extendHead(SingleScreenshot()) {
|
||||
this.outputFile = System.getProperty("screenshotPath")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
orx-jvm/orx-olive/src/demo/kotlin/demo-olive-01.kts
Normal file
14
orx-jvm/orx-olive/src/demo/kotlin/demo-olive-01.kts
Normal file
@@ -0,0 +1,14 @@
|
||||
@file:Suppress("UNUSED_LAMBDA_EXPRESSION")
|
||||
import org.openrndr.Program
|
||||
import org.openrndr.color.ColorRGBa
|
||||
import org.openrndr.draw.*
|
||||
|
||||
{ program: Program ->
|
||||
program.apply {
|
||||
extend {
|
||||
drawer.clear(ColorRGBa.GRAY)
|
||||
drawer.fill = ColorRGBa.PINK
|
||||
drawer.circle(width/2.0, height/2.0 ,200.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
193
orx-jvm/orx-olive/src/main/kotlin/Olive.kt
Normal file
193
orx-jvm/orx-olive/src/main/kotlin/Olive.kt
Normal file
@@ -0,0 +1,193 @@
|
||||
package org.openrndr.extra.olive
|
||||
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.async
|
||||
import mu.KotlinLogging
|
||||
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.operndr.extras.filewatcher.stop
|
||||
import org.operndr.extras.filewatcher.triggerChange
|
||||
import org.operndr.extras.filewatcher.watchFile
|
||||
import java.io.File
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
|
||||
private fun <T> Event<T>.saveListeners(store: MutableMap<Event<*>, List<(Any) -> Unit>>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
store[this] = listeners.map { it } as List<(Any) -> Unit>
|
||||
}
|
||||
|
||||
private fun <T> Event<T>.restoreListeners(store: Map<Event<*>, List<(Any) -> Unit>>) {
|
||||
listeners.retainAll(store[this] ?: emptyList<T>())
|
||||
}
|
||||
|
||||
enum class OliveScriptHost {
|
||||
JSR223,
|
||||
JSR223_REUSE,
|
||||
KOTLIN_SCRIPT
|
||||
}
|
||||
|
||||
data class ScriptLoadedEvent(val scriptFile: String)
|
||||
|
||||
enum class ScriptMode {
|
||||
KOTLIN_SCRIPT,
|
||||
OLIVE_PROGRAM
|
||||
}
|
||||
|
||||
class Olive<P : Program>(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<ScriptLoadedEvent>()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
private var watcher: (() -> Unit)? = null
|
||||
|
||||
override fun setup(program: Program) {
|
||||
System.setProperty("idea.io.use.fallback", "true")
|
||||
System.setProperty("org.openrndr.ignoreShadeStyleErrors", "true")
|
||||
|
||||
val store = mutableMapOf<Event<*>, List<(Any) -> Unit>>()
|
||||
val originalExtensions = program.extensions.map { it }
|
||||
val trackedListeners = listOf<Event<*>>(program.mouse.buttonDown,
|
||||
program.mouse.buttonUp,
|
||||
program.mouse.clicked,
|
||||
program.mouse.dragged,
|
||||
program.mouse.moved,
|
||||
program.mouse.scrolled,
|
||||
program.keyboard.keyUp,
|
||||
program.keyboard.keyDown,
|
||||
program.keyboard.keyRepeat,
|
||||
program.window.drop,
|
||||
program.window.focused,
|
||||
program.window.minimized,
|
||||
program.window.moved,
|
||||
program.window.sized,
|
||||
program.window.unfocused)
|
||||
|
||||
trackedListeners.forEach { it.saveListeners(store) }
|
||||
|
||||
fun setupScript(scriptFile: String) {
|
||||
watcher?.stop()
|
||||
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 = program.watchFile(File(script)) {
|
||||
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<OliveProgram>(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<P.() -> 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)
|
||||
|
||||
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
|
||||
|
||||
program.watchFile(file) {
|
||||
if (resources[file]!! && filePath != null) {
|
||||
file.copyTo(destFile, overwrite = true)
|
||||
reload()
|
||||
} else {
|
||||
resources[file] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
orx-jvm/orx-olive/src/main/kotlin/OliveProgram.kt
Normal file
44
orx-jvm/orx-olive/src/main/kotlin/OliveProgram.kt
Normal file
@@ -0,0 +1,44 @@
|
||||
package org.openrndr.extra.olive
|
||||
|
||||
import org.openrndr.ApplicationBuilder
|
||||
import org.openrndr.Program
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import kotlin.streams.toList
|
||||
|
||||
open class OliveProgram(private val sourceLocation: String, private val scriptHost: OliveScriptHost, resources: Resources?) : Program() {
|
||||
val olive = extend(Olive<OliveProgram>(scriptMode = ScriptMode.OLIVE_PROGRAM, resources = resources)) {
|
||||
script = sourceLocation
|
||||
scriptHost = this@OliveProgram.scriptHost
|
||||
}
|
||||
}
|
||||
|
||||
fun stackRootClassName(thread: Thread = Thread.currentThread(), sanitize: Boolean = true): String {
|
||||
val root = Thread.currentThread().stackTrace.last()
|
||||
val rootClass = root.className
|
||||
return if (sanitize) rootClass.replace(Regex("Kt$"), "") else rootClass
|
||||
}
|
||||
|
||||
fun ApplicationBuilder.oliveProgram(scriptHost: OliveScriptHost = OliveScriptHost.JSR223_REUSE, resources: Resources? = null, init: OliveProgram.() -> Unit): OliveProgram {
|
||||
val rootClassName = stackRootClassName(sanitize = true).split(".").last()
|
||||
|
||||
var sourceLocation = "src/main/kotlin/$rootClassName.kt"
|
||||
val candidateFile = File(sourceLocation)
|
||||
if (!candidateFile.exists()) {
|
||||
val otherCandidates = Files.walk(Paths.get("."))
|
||||
.filter { Files.isRegularFile(it) && it.toString().endsWith("$rootClassName.kt") }.toList()
|
||||
if (otherCandidates.size == 1) {
|
||||
sourceLocation = otherCandidates.first().toString()
|
||||
} else {
|
||||
error("multiple source candidates found: $otherCandidates")
|
||||
}
|
||||
}
|
||||
program = object : OliveProgram(sourceLocation, scriptHost, resources) {
|
||||
override fun setup() {
|
||||
super.setup()
|
||||
init()
|
||||
}
|
||||
}
|
||||
return program as OliveProgram
|
||||
}
|
||||
55
orx-jvm/orx-olive/src/main/kotlin/Reloadable.kt
Normal file
55
orx-jvm/orx-olive/src/main/kotlin/Reloadable.kt
Normal file
@@ -0,0 +1,55 @@
|
||||
package org.openrndr.extra.olive
|
||||
|
||||
import mu.KotlinLogging
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
import kotlin.reflect.jvm.jvmName
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private val store = mutableMapOf<String, Any>()
|
||||
|
||||
/**
|
||||
* Clear reloadable values
|
||||
*/
|
||||
fun clearReloadables() {
|
||||
store.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* A class with which persistent state can be reloaded from inside Olive scripts.
|
||||
*/
|
||||
open class Reloadable {
|
||||
private fun normalizeClassName(name: String): String {
|
||||
return name.replace(Regex("ScriptingHost[0-9a-f]+_"), // -- since kotlin 1.3.61 the scripting host prepends class names with the host id
|
||||
"").replace(Regex("Line_[0-9]+"),"") // -- when reusing the script engine the line number increments.
|
||||
}
|
||||
|
||||
/**
|
||||
* reload property values from store
|
||||
*/
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun reload() {
|
||||
val className = normalizeClassName(this::class.jvmName)
|
||||
val existing = store[className]
|
||||
if (existing != null) {
|
||||
for (p in this::class.declaredMemberProperties) {
|
||||
val e = existing::class.declaredMemberProperties.find { it.name == p.name }
|
||||
if (e != null) {
|
||||
try {
|
||||
val value = (e as KProperty1<Any, Any?>).get(existing)
|
||||
val mp = (p as KMutableProperty1<Any, Any?>)
|
||||
mp.set(this, value as Any)
|
||||
logger.info("reloaded property ${p.name} <- ${value}")
|
||||
} catch (e: Throwable) {
|
||||
logger.warn("error while reloading property ${p.name}: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.info("no existing store found for $className")
|
||||
}
|
||||
store[normalizeClassName(this::class.jvmName)] = this
|
||||
}
|
||||
}
|
||||
29
orx-jvm/orx-olive/src/main/kotlin/Resources.kt
Normal file
29
orx-jvm/orx-olive/src/main/kotlin/Resources.kt
Normal file
@@ -0,0 +1,29 @@
|
||||
package org.openrndr.extra.olive
|
||||
|
||||
import java.io.File
|
||||
|
||||
class Resources(val filterOutExtensions: List<String> = listOf()) {
|
||||
private val watchedResources = mutableMapOf<File, Boolean>()
|
||||
|
||||
fun watch(src: File, watchFn: (file: File) -> Unit) {
|
||||
src.listFiles()!!.forEach {file ->
|
||||
if (file.isFile && !filterOutExtensions.contains(file.extension)) {
|
||||
watchedResources[file] = false
|
||||
|
||||
watchFn(file)
|
||||
} else if (file.isDirectory) {
|
||||
watch(file, watchFn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operator fun get(file: File): Boolean? {
|
||||
return watchedResources[file]
|
||||
}
|
||||
|
||||
operator fun set(file: File, value: Boolean) {
|
||||
if (watchedResources.containsKey(file)) {
|
||||
watchedResources[file] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
20
orx-jvm/orx-olive/src/main/kotlin/ScriptGenerator.kt
Normal file
20
orx-jvm/orx-olive/src/main/kotlin/ScriptGenerator.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package org.openrndr.extra.olive
|
||||
|
||||
import org.openrndr.extra.kotlinparser.ProgramSource
|
||||
|
||||
inline fun <reified T> generateScript(programSource: ProgramSource): String {
|
||||
val script = """
|
||||
|
||||
//${programSource.packageName?:""}
|
||||
|
||||
import org.openrndr.extra.olive.OliveProgram
|
||||
${programSource.imports}
|
||||
|
||||
{ program: ${T::class.qualifiedName} ->
|
||||
program.apply {
|
||||
${programSource.programLambda}
|
||||
}
|
||||
}
|
||||
"""
|
||||
return script
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.openrndr.extra.olive
|
||||
|
||||
import mu.KotlinLogging
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.Reader
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import javax.script.ScriptEngineManager
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
class LoadException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause)
|
||||
|
||||
class ScriptObjectLoader(classLoader: ClassLoader? = Thread.currentThread().contextClassLoader) {
|
||||
val engine = run {
|
||||
val start = System.currentTimeMillis()
|
||||
val engine = ScriptEngineManager(classLoader).getEngineByExtension("kts")
|
||||
val end = System.currentTimeMillis()
|
||||
logger.info { "creating scripting engine took ${end-start}ms" }
|
||||
engine
|
||||
}
|
||||
|
||||
init {
|
||||
require(engine != null) { "could not create scripting engine" }
|
||||
}
|
||||
|
||||
fun <R> safeEval(evaluation: () -> R?) = try {
|
||||
evaluation()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
throw LoadException("Cannot load script", e)
|
||||
}
|
||||
|
||||
inline fun <reified T> Any?.castOrError() = takeIf { it is T }?.let { it as T }
|
||||
?: throw IllegalArgumentException("Cannot cast $this to expected type ${T::class}")
|
||||
|
||||
inline fun <reified T> load(script: String): T = safeEval { engine.eval(script) }.castOrError()
|
||||
|
||||
inline fun <reified T> load(reader: Reader): T = safeEval { engine.eval(reader) }.castOrError()
|
||||
|
||||
inline fun <reified T> load(inputStream: InputStream): T = load(inputStream.reader())
|
||||
|
||||
inline fun <reified T> loadAll(vararg inputStream: InputStream): List<T> = inputStream.map(::load)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load an object from script.
|
||||
*/
|
||||
inline fun <reified T : Any> loadFromScript(fileOrUrl: String, loader: ScriptObjectLoader = ScriptObjectLoader()): T {
|
||||
val isUrl = try {
|
||||
URL(fileOrUrl); true
|
||||
} catch (e: MalformedURLException) {
|
||||
false
|
||||
}
|
||||
|
||||
val script = if (isUrl) {
|
||||
URL(fileOrUrl).readText()
|
||||
} else {
|
||||
File(fileOrUrl).readText()
|
||||
}
|
||||
return loader.load(script)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an object from script file
|
||||
*/
|
||||
inline fun <reified T : Any> loadFromScript(file: File, loader: ScriptObjectLoader = ScriptObjectLoader()): T =
|
||||
loader.load(file.readText())
|
||||
|
||||
|
||||
/**
|
||||
* Load an object from script file
|
||||
*/
|
||||
inline fun <reified T : Any> loadFromScriptContents(contents:String, loader: ScriptObjectLoader = ScriptObjectLoader()): T =
|
||||
loader.load(contents)
|
||||
49
orx-jvm/orx-olive/src/main/kotlin/ScriptObjectLoaderKSH.kt
Normal file
49
orx-jvm/orx-olive/src/main/kotlin/ScriptObjectLoaderKSH.kt
Normal file
@@ -0,0 +1,49 @@
|
||||
package org.openrndr.extra.olive
|
||||
|
||||
import java.io.File
|
||||
import kotlin.script.experimental.api.*
|
||||
import kotlin.script.experimental.host.BasicScriptingHost
|
||||
import kotlin.script.experimental.host.toScriptSource
|
||||
import kotlin.script.experimental.jvm.dependenciesFromCurrentContext
|
||||
import kotlin.script.experimental.jvm.jvm
|
||||
import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost
|
||||
import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate
|
||||
import kotlin.script.templates.standard.SimpleScriptTemplate
|
||||
|
||||
internal fun evalScriptWithConfiguration(
|
||||
script: String,
|
||||
host: BasicScriptingHost = BasicJvmScriptingHost(),
|
||||
body: ScriptCompilationConfiguration.Builder.() -> Unit = {}
|
||||
): ResultWithDiagnostics<EvaluationResult> {
|
||||
val compilationConfiguration = createJvmCompilationConfigurationFromTemplate<SimpleScriptTemplate>(body = body)
|
||||
return host.eval(script.toScriptSource(), compilationConfiguration, null)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> loadFromScriptKSH(
|
||||
script: File,
|
||||
host: BasicScriptingHost = BasicJvmScriptingHost(),
|
||||
body: ScriptCompilationConfiguration.Builder.() -> Unit = {
|
||||
|
||||
jvm {
|
||||
dependenciesFromCurrentContext(wholeClasspath = true)
|
||||
}
|
||||
|
||||
}
|
||||
): T = loadFromScriptContentsKSH(script.readText(), host, body)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> loadFromScriptContentsKSH(
|
||||
script: String,
|
||||
host: BasicScriptingHost = BasicJvmScriptingHost(),
|
||||
body: ScriptCompilationConfiguration.Builder.() -> Unit = {
|
||||
|
||||
jvm {
|
||||
dependenciesFromCurrentContext(wholeClasspath = true)
|
||||
}
|
||||
|
||||
}
|
||||
): T = (evalScriptWithConfiguration(script, host, body).valueOrThrow().returnValue as ResultValue.Value).value as T
|
||||
|
||||
|
||||
|
||||
18
orx-jvm/orx-olive/src/test/kotlin/TestLoadScript.kt
Normal file
18
orx-jvm/orx-olive/src/test/kotlin/TestLoadScript.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.openrndr.extra.olive.ScriptObjectLoader
|
||||
import org.openrndr.extra.olive.loadFromScript
|
||||
import org.spekframework.spek2.Spek
|
||||
import org.spekframework.spek2.style.specification.describe
|
||||
|
||||
object TestLoadScript : Spek({
|
||||
|
||||
describe("some script") {
|
||||
val loader = ScriptObjectLoader()
|
||||
|
||||
val number = loader.load<Int>("5")
|
||||
|
||||
it("should evaluate properly") {
|
||||
number `should be equal to` 5
|
||||
}
|
||||
}
|
||||
})
|
||||
15
orx-jvm/orx-olive/src/test/kotlin/TestLoadScriptKSH.kt
Normal file
15
orx-jvm/orx-olive/src/test/kotlin/TestLoadScriptKSH.kt
Normal file
@@ -0,0 +1,15 @@
|
||||
import org.amshove.kluent.`should be equal to`
|
||||
import org.openrndr.extra.olive.loadFromScriptContentsKSH
|
||||
import org.spekframework.spek2.Spek
|
||||
import org.spekframework.spek2.style.specification.describe
|
||||
|
||||
object TestLoadScriptKSH : Spek({
|
||||
|
||||
describe("some script") {
|
||||
val number = loadFromScriptContentsKSH<Int>("5")
|
||||
|
||||
it("should evaluate properly") {
|
||||
number `should be equal to` 5
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user