Upgrade to OPENRNDR 0.4 snapshot

This commit is contained in:
Edwin Jakobs
2021-06-22 11:08:07 +02:00
parent 579ddf9bb5
commit 9435907ef9
339 changed files with 460 additions and 497 deletions

146
orx-jvm/orx-olive/README.md Normal file
View 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)
![DemoOlive01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-olive/images/DemoOlive01Kt.png)
### DemoOliveScriptless01
[source code](src/demo/kotlin/DemoOliveScriptless01.kt)
![DemoOliveScriptless01Kt](https://raw.githubusercontent.com/openrndr/orx/media/orx-olive/images/DemoOliveScriptless01Kt.png)

View 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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View 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")
}
}
}
}
}
}

View 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")
}
}
}
}
}

View 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)
}
}
}

View 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
}
}
}
}
}
}

View 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
}

View 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
}
}

View 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
}
}
}

View 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
}

View File

@@ -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)

View 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

View 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
}
}
})

View 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
}
}
})