[orx-parameters, orx-property-watchers, orx-file-watcher, orx-gui] Add @PathParameter, file watcher delegates and property delegates

This commit is contained in:
Edwin Jakobs
2023-03-18 20:32:43 +01:00
parent bab525cd92
commit 84e623c3e8
20 changed files with 553 additions and 143 deletions

View File

@@ -23,6 +23,7 @@ def multiplatformModules = [
"orx-no-clear", "orx-no-clear",
"orx-noise", "orx-noise",
"orx-parameters", "orx-parameters",
"orx-property-watchers",
"orx-shade-styles", "orx-shade-styles",
"orx-shader-phrases", "orx-shader-phrases",
"orx-shapes", "orx-shapes",

View File

@@ -4,109 +4,22 @@ import com.sun.nio.file.SensitivityWatchEventModifier
import kotlinx.coroutines.* import kotlinx.coroutines.*
import mu.KotlinLogging import mu.KotlinLogging
import org.openrndr.Program import org.openrndr.Program
import org.openrndr.events.Event
import org.openrndr.launch import org.openrndr.launch
import java.io.File import java.io.File
import java.nio.file.FileSystems import java.nio.file.FileSystems
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.StandardWatchEventKinds import java.nio.file.StandardWatchEventKinds
import java.nio.file.WatchKey import java.nio.file.WatchKey
import java.util.WeakHashMap
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.reflect.KProperty
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
class FileWatcher(private val program: Program, val file: File, private val onChange: (File) -> Unit) {
val path = file.absoluteFile.toPath()
val parent = path.parent
val key = pathKeys.getOrPut(parent) {
parent.register(
watchService, arrayOf(StandardWatchEventKinds.ENTRY_MODIFY),
SensitivityWatchEventModifier.HIGH
)
}
val watchers = mutableListOf<() -> Unit>()
init {
watchThread
watching.getOrPut(path) {
mutableListOf()
}.add(this)
keyPaths.getOrPut(key) { parent }
}
fun stop() {
watching[path]?.remove(this)
}
internal fun triggerChange() {
program.launch {
onChange(file)
watchers.forEach { it() }
}
}
}
private val watchers = mutableMapOf<() -> Any, FileWatcher>()
fun <T> watchFile(program: Program, file: File, transducer: (File) -> T): () -> T {
var result = transducer(file)
val watcher = FileWatcher(program, file) {
try {
result = transducer(file)
} catch (e: Throwable) {
logger.error(e) {
"""exception while transducing file"""
}
}
}
val function = {
result
}
@Suppress("UNCHECKED_CAST")
watchers[function as () -> Any] = watcher
return function
}
/**
* Stops the watcher
*/
fun <T> (() -> T).stop() {
@Suppress("UNCHECKED_CAST")
watchers[this as () -> Any]?.stop()
}
/**
* Triggers reload
*/
fun <T> (() -> T).triggerChange() {
@Suppress("UNCHECKED_CAST")
watchers[this as () -> Any]?.triggerChange()
}
/**
* add watcher to file watcher
*/
fun <T, R> (() -> T).watch(transducer: (T) -> R): () -> R {
var result = transducer(this())
@Suppress("USELESS_CAST")
watchers[this as () -> Any?]!!.watchers.add {
result = transducer(this())
}
return { result }
}
@JvmName("programWatchFile")
fun <T> Program.watchFile(file: File, transducer: (File) -> T): () -> T = watchFile(this, file, transducer)
private val watching = mutableMapOf<Path, MutableList<FileWatcher>>() private val watching = mutableMapOf<Path, MutableList<FileWatcher>>()
private val pathKeys = mutableMapOf<Path, WatchKey>() private val pathKeys = mutableMapOf<Path, WatchKey>()
private val keyPaths = mutableMapOf<WatchKey, Path>() private val keyPaths = WeakHashMap<WatchKey, Path>()
private val waiting = mutableMapOf<Path, Job>() private val waiting = mutableMapOf<Path, Job>()
private val watchService by lazy { private val watchService by lazy {
@@ -119,13 +32,13 @@ private val watchThread by lazy {
while (true) { while (true) {
val key = watchService.take() val key = watchService.take()
val path = keyPaths[key] val path = keyPaths[key]
key.pollEvents().forEach { key.pollEvents().forEach {
val contextPath = it.context() as Path val contextPath = it.context() as Path
val fullPath = path?.resolve(contextPath) val fullPath = path?.resolve(contextPath)
fullPath?.let { fullPath?.let {
waiting[fullPath]?.cancel() waiting[fullPath]?.cancel()
waiting[fullPath] = GlobalScope.launch { waiting[fullPath] = GlobalScope.launch {
delay(100) delay(100)
watching[fullPath]?.forEach { w -> watching[fullPath]?.forEach { w ->
@@ -138,3 +51,73 @@ private val watchThread by lazy {
} }
} }
} }
class FileWatcher(
val file: File,
private val fileChangedEvent: Event<File>,
requestStopEvent: Event<Unit>? = null
) {
val path = file.absoluteFile.toPath()
val parent = path.parent
val key = pathKeys.getOrPut(parent) {
parent.register(
watchService, arrayOf(StandardWatchEventKinds.ENTRY_MODIFY),
SensitivityWatchEventModifier.HIGH
)
}
init {
watchThread
watching.getOrPut(path) {
mutableListOf()
}.add(this)
keyPaths.getOrPut(key) { parent }
requestStopEvent?.listenOnce {
stop()
}
}
fun stop() {
synchronized(watching) {
logger.info { "stopping, watcher stop requested" }
watching[path]?.remove(this)
}
}
internal fun triggerChange() {
fileChangedEvent.trigger(file)
}
}
fun <T> watchFile(
file: File,
contentsChangedEvent: Event<T>? = null,
requestStopEvent: Event<Unit>? = null,
transducer: (File) -> T
): () -> T {
var result = transducer(file)
val fileChangedEvent = Event<File>()
val watcher = FileWatcher(file, fileChangedEvent, requestStopEvent)
fileChangedEvent.listen {
try {
result = transducer(file)
contentsChangedEvent?.trigger(result)
} catch (e: Throwable) {
logger.error(e) {
"""exception while transducing file"""
}
}
}
return {
result
}
}
//@JvmName("programWatchFile")
//fun <T> Program.watchFile(file: File, onChange: Event<T>? = null, transducer: (File) -> T): () -> T =
// watchFile(this, file, onChange, transducer = transducer)

View File

@@ -0,0 +1,38 @@
import kotlinx.coroutines.yield
import org.openrndr.Program
import org.openrndr.events.Event
import org.openrndr.extra.filewatcher.watchFile
import org.openrndr.launch
import java.io.File
import kotlin.reflect.KProperty
class FileWatcherDelegate<T>(
program: Program,
file: File,
valueChangedEvent: Event<T>? = null,
requestStopEvent: Event<Unit>? = null,
transducer: (File) -> T
) {
val watchValue = watchFile(file, valueChangedEvent, requestStopEvent, transducer)
var value = watchValue()
init {
program.launch {
while (true) {
value = watchValue()
yield()
}
}
}
operator fun getValue(any: Any, property: KProperty<*>): T {
return value
}
}
fun <R> Program.watchingFile(
file: File,
valueChangedEvent: Event<R>? = null,
requestStopEvent: Event<Unit>? = null,
transducer: (File) -> R
) = FileWatcherDelegate(this, file, valueChangedEvent, requestStopEvent, transducer)

View File

@@ -13,6 +13,7 @@ dependencies {
api(project(":orx-parameters")) api(project(":orx-parameters"))
api(project(":orx-jvm:orx-panel")) api(project(":orx-jvm:orx-panel"))
api(project(":orx-noise")) api(project(":orx-noise"))
demoImplementation(project(":orx-property-watchers"))
implementation(libs.openrndr.application) implementation(libs.openrndr.application)
implementation(libs.openrndr.math) implementation(libs.openrndr.math)
implementation(libs.openrndr.filter) implementation(libs.openrndr.filter)

View File

@@ -0,0 +1,28 @@
import org.openrndr.application
import org.openrndr.extra.gui.GUI
import org.openrndr.extra.parameters.Description
import org.openrndr.extra.parameters.PathParameter
import org.openrndr.extra.propertywatchers.watchingImagePath
fun main() {
application {
program {
val gui = GUI()
gui.compartmentsCollapsedByDefault = false
val settings = @Description("Settings") object {
@PathParameter("image", extensions = ["jpg", "png"], order = 10)
var imagePath = "demo-data/image-001.png"
val image by watchingImagePath(::imagePath) {
it
}
}
gui.add(settings)
extend(gui)
extend {
drawer.image(settings.image)
}
}
}
}

View File

@@ -11,12 +11,6 @@ import org.openrndr.shape.Circle
*/ */
fun main() = application { fun main() = application {
program { program {
// -- this block is for automation purposes only
if (System.getProperty("takeScreenshot") == "true") {
extend(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath")
}
}
val gui = GUI() val gui = GUI()
gui.compartmentsCollapsedByDefault = false gui.compartmentsCollapsedByDefault = false

View File

@@ -1,14 +1,12 @@
package org.openrndr.extra.gui package org.openrndr.extra.gui
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import mu.KotlinLogging import mu.KotlinLogging
import org.openrndr.* import org.openrndr.*
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.dialogs.getDefaultPathForContext import org.openrndr.dialogs.*
import org.openrndr.dialogs.openFileDialog
import org.openrndr.dialogs.saveFileDialog
import org.openrndr.dialogs.setDefaultPathForContext
import org.openrndr.draw.Drawer import org.openrndr.draw.Drawer
import org.openrndr.extra.noise.Random import org.openrndr.extra.noise.Random
import org.openrndr.extra.noise.random import org.openrndr.extra.noise.random
@@ -58,7 +56,7 @@ private fun <T : Any> getPersistedOrDefault(
compartmentLabel: String, compartmentLabel: String,
property: KMutableProperty1<Any, T>, property: KMutableProperty1<Any, T>,
obj: Any obj: Any
): T? { ): T {
val state = persistentCompartmentStates[Driver.instance.contextID]!![compartmentLabel] val state = persistentCompartmentStates[Driver.instance.contextID]!![compartmentLabel]
if (state == null) { if (state == null) {
return property.get(obj) return property.get(obj)
@@ -83,7 +81,7 @@ class GUIAppearance(val baseColor: ColorRGBa = ColorRGBa.GRAY, val barWidth: Int
class GUI( class GUI(
val appearance: GUIAppearance = GUIAppearance(), val appearance: GUIAppearance = GUIAppearance(),
val defaultStyles: List<StyleSheet> = defaultStyles(), val defaultStyles: List<StyleSheet> = defaultStyles(),
) : Extension { ) : Extension {
private var onChangeListener: ((name: String, value: Any?) -> Unit)? = null private var onChangeListener: ((name: String, value: Any?) -> Unit)? = null
override var enabled = true override var enabled = true
@@ -593,6 +591,47 @@ class GUI(
} }
} }
ParameterType.Path -> {
button {
label = "Load ${parameter.label}"
clicked {
if (parameter.pathIsDirectory == false) {
openFileDialog(
supportedExtensions = parameter.pathExtensions?.toList() ?: emptyList(),
contextID = parameter.pathContext ?: "null"
) {
val resolvedPath = if (parameter.absolutePath == true) {
it.absolutePath
} else {
it.relativeTo(File(".").absoluteFile).path
}
setAndPersist(
compartment.label,
parameter.property as KMutableProperty1<Any, String>,
obj,
resolvedPath
)
}
} else {
openFolderDialog(contextID = parameter.pathContext ?: "null") {
val resolvedPath = if (parameter.absolutePath == true) {
it.absolutePath
} else {
it.relativeTo(File(".").absoluteFile).path
}
setAndPersist(
compartment.label,
parameter.property as KMutableProperty1<Any, String>,
obj,
resolvedPath
)
}
}
}
}
}
ParameterType.DoubleList -> { ParameterType.DoubleList -> {
sequenceEditor { sequenceEditor {
range = parameter.doubleRange!! range = parameter.doubleRange!!
@@ -731,7 +770,10 @@ class GUI(
it.value.data as? Enum<*> ?: error("no data") it.value.data as? Enum<*> ?: error("no data")
) )
onChangeListener?.invoke(parameter.property!!.name, it.value.data as? Enum<*> ?: error("no data")) onChangeListener?.invoke(
parameter.property!!.name,
it.value.data as? Enum<*> ?: error("no data")
)
} }
getPersistedOrDefault( getPersistedOrDefault(
compartment.label, compartment.label,
@@ -838,6 +880,8 @@ class GUI(
maxValue = k.doubleRange?.endInclusive maxValue = k.doubleRange?.endInclusive
) )
ParameterType.Path -> ParameterValue(textValue = k.property.qget(lo.obj) as String)
ParameterType.Option -> ParameterValue(optionValue = (k.property.qget(lo.obj) as Enum<*>).name) ParameterType.Option -> ParameterValue(optionValue = (k.property.qget(lo.obj) as Enum<*>).name)
} }
) )
@@ -919,6 +963,10 @@ class GUI(
parameter.property.enumSet(lo.obj, it) parameter.property.enumSet(lo.obj, it)
} }
ParameterType.Path -> parameterValue.textValue?.let {
parameter.property.qset(lo.obj, it)
}
ParameterType.Action -> { ParameterType.Action -> {
// intentionally do nothing // intentionally do nothing
} }
@@ -933,7 +981,12 @@ class GUI(
fun loadParameters(file: File) { fun loadParameters(file: File) {
val json = file.readText() val json = file.readText()
val typeToken = object : TypeToken<Map<String, Map<String, ParameterValue>>>() {} val typeToken = object : TypeToken<Map<String, Map<String, ParameterValue>>>() {}
val labeledValues: Map<String, Map<String, ParameterValue>> = Gson().fromJson(json, typeToken.type) val labeledValues: Map<String, Map<String, ParameterValue>> = try {
Gson().fromJson(json, typeToken.type)
} catch (e: JsonSyntaxException) {
println("could not parse json: $json")
throw e;
}
fromObject(labeledValues) fromObject(labeledValues)
} }
@@ -998,6 +1051,10 @@ class GUI(
} ?: error("could not find item") } ?: error("could not find item")
} }
ParameterType.Path -> {
}
ParameterType.Action -> { ParameterType.Action -> {
// intentionally do nothing // intentionally do nothing
} }

View File

@@ -12,32 +12,33 @@ fun main() {
width = 1280 width = 1280
height = 720 height = 720
} }
oliveProgram(scriptHost = OliveScriptHost.JSR223) { oliveProgram(scriptHost = OliveScriptHost.JSR223_REUSE) {
extend { extend {
drawer.clear(ColorRGBa.GRAY) drawer.clear(ColorRGBa.GRAY)
drawer.fill = ColorRGBa.WHITE drawer.fill = ColorRGBa.WHITE
for (i in 0 until 100) { for (i in 0 until 100) {
drawer.circle( drawer.circle(
width / 2.0 + cos(seconds + i) * 320.0, width / 2.0 + cos(seconds + i) * 320.0,
i * 7.2, i * 7.2,
cos(i + seconds * 0.5) * 20.0 + 20.0) cos(i + seconds * 0.5) * 20.0 + 20.0
)
} }
} }
} }
// -- this is only needed for the automated screenshots // -- this is only needed for the automated screenshots
.olive.scriptLoaded.listen { .olive.scriptLoaded.listen {
if (System.getProperty("takeScreenshot") == "true") { if (System.getProperty("takeScreenshot") == "true") {
// -- this is a bit of hack, we need to push the screenshot extension in front of the olive one // -- 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 { fun <T : Extension> extendHead(extension: T, configure: T.() -> Unit): T {
program.extensions.add(0, extension) program.extensions.add(0, extension)
extension.configure() extension.configure()
extension.setup(program) extension.setup(program)
return extension return extension
} }
extendHead(SingleScreenshot()) { extendHead(SingleScreenshot()) {
this.outputFile = System.getProperty("screenshotPath") this.outputFile = System.getProperty("screenshotPath")
}
} }
} }
}
} }
} }

View File

@@ -11,8 +11,6 @@ import org.openrndr.events.Event
import org.openrndr.exceptions.stackRootClassName import org.openrndr.exceptions.stackRootClassName
import org.openrndr.extra.kotlinparser.extractProgram import org.openrndr.extra.kotlinparser.extractProgram
import org.openrndr.launch import org.openrndr.launch
import org.openrndr.extra.filewatcher.stop
import org.openrndr.extra.filewatcher.triggerChange
import org.openrndr.extra.filewatcher.watchFile import org.openrndr.extra.filewatcher.watchFile
import java.io.File import java.io.File
@@ -63,9 +61,14 @@ class Olive<P : Program>(val resources: Resources? = null, private var scriptMod
* reloads the active script * reloads the active script
*/ */
fun reload() { fun reload() {
watcher?.triggerChange() // watcher?.triggerChange()
} }
class ScriptWatcher
private var watcherRequestStopEvent = Event<Unit>()
private var watcher: (() -> Unit)? = null private var watcher: (() -> Unit)? = null
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
@@ -99,7 +102,12 @@ class Olive<P : Program>(val resources: Resources? = null, private var scriptMod
val originalAssetProperties = program.assetProperties.toMutableMap() val originalAssetProperties = program.assetProperties.toMutableMap()
fun setupScript(scriptFile: String) { fun setupScript(scriptFile: String) {
watcher?.stop() if (watcher != null) {
logger.info { "requesting watcher stop" }
watcherRequestStopEvent.trigger(Unit)
} else {
logger.info { "no existing watcher" }
}
val f = File(scriptFile) val f = File(scriptFile)
if (!f.exists()) { if (!f.exists()) {
f.parentFile.mkdirs() f.parentFile.mkdirs()
@@ -124,7 +132,7 @@ class Olive<P : Program>(val resources: Resources? = null, private var scriptMod
val jsr233ObjectLoader = if (scriptHost == OliveScriptHost.JSR223_REUSE) ScriptObjectLoader() else null val jsr233ObjectLoader = if (scriptHost == OliveScriptHost.JSR223_REUSE) ScriptObjectLoader() else null
watcher = program.watchFile(File(script)) { watcher = watchFile(File(script), requestStopEvent = watcherRequestStopEvent) {
try { try {
logger.info("change detected, reloading script") logger.info("change detected, reloading script")
@@ -188,7 +196,7 @@ class Olive<P : Program>(val resources: Resources? = null, private var scriptMod
val destFile = File("$dest/${filePath}").absoluteFile val destFile = File("$dest/${filePath}").absoluteFile
program.watchFile(file) { watchFile(file) {
if (resources[file]!! && filePath != null) { if (resources[file]!! && filePath != null) {
file.copyTo(destFile, overwrite = true) file.copyTo(destFile, overwrite = true)
reload() reload()

View File

@@ -3,14 +3,7 @@ plugins {
} }
kotlin { kotlin {
jvm { sourceSets {
testRuns["test"].executionTask {
useJUnitPlatform {
includeEngines("spek2")
}
}
}
sourceSets {
@Suppress("UNUSED_VARIABLE") @Suppress("UNUSED_VARIABLE")
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
@@ -25,8 +18,6 @@ kotlin {
val jvmTest by getting { val jvmTest by getting {
dependencies { dependencies {
implementation(libs.kluent) implementation(libs.kluent)
implementation(libs.spek.dsl)
runtimeOnly(libs.spek.junit5)
runtimeOnly(libs.kotlin.reflect) runtimeOnly(libs.kotlin.reflect)
} }
} }

View File

@@ -159,6 +159,29 @@ annotation class Vector4Parameter(
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class ActionParameter(val label: String, val order: Int = Int.MAX_VALUE) annotation class ActionParameter(val label: String, val order: Int = Int.MAX_VALUE)
/**
* ActionParameter annotation for functions without arguments
* @property label a short description of the parameter
* @property absolute should the path be stored as an absolute path
* @property context which dialog context to use
* @property extensions an array of supported extensions
* @property directory the path points to a directory
* @property order hint for where to place the parameter in user interfaces
*/
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class PathParameter(
val label: String,
val absolute: Boolean = false,
val context: String = "null",
val extensions: Array<String> = [],
val directory: Boolean = false,
val order: Int = Int.MAX_VALUE
)
//</editor-fold> //</editor-fold>
//<editor-fold desc="2. Add an entry to ParameterType" id="add-parameter-type" defaultstate="collapsed"> //<editor-fold desc="2. Add an entry to ParameterType" id="add-parameter-type" defaultstate="collapsed">
enum class ParameterType(val annotationClass: KClass<out Annotation>) { enum class ParameterType(val annotationClass: KClass<out Annotation>) {
@@ -173,7 +196,8 @@ enum class ParameterType(val annotationClass: KClass<out Annotation>) {
Vector2(Vector2Parameter::class), Vector2(Vector2Parameter::class),
Vector3(Vector3Parameter::class), Vector3(Vector3Parameter::class),
Vector4(Vector4Parameter::class), Vector4(Vector4Parameter::class),
Option(OptionParameter::class) Option(OptionParameter::class),
Path(PathParameter::class)
; ;
companion object { companion object {
@@ -209,7 +233,12 @@ class Parameter(
val invertY: Boolean?, val invertY: Boolean?,
val showVector: Boolean?, val showVector: Boolean?,
// val optionEnum: Enum<*>, // val optionEnum: Enum<*>,
val order: Int) val absolutePath: Boolean?,
val pathContext: String?,
val pathExtensions: Array<String>?,
val pathIsDirectory: Boolean?,
val order: Int,
)
//</editor-fold> //</editor-fold>
//<editor-fold desc="4. Add handling annotation code to listParameters" defaultstate="collapsed"> //<editor-fold desc="4. Add handling annotation code to listParameters" defaultstate="collapsed">
/** /**

View File

@@ -31,6 +31,10 @@ fun Any.listParameters(): List<Parameter> {
var vectorRange = Pair(Vector2(-1.0, -1.0), Vector2(1.0, 1.0)) var vectorRange = Pair(Vector2(-1.0, -1.0), Vector2(1.0, 1.0))
var invertY: Boolean? = null var invertY: Boolean? = null
var showVector: Boolean? = null var showVector: Boolean? = null
var absolutePath: Boolean? = null
var pathContext: String? = null
var pathExtensions: Array<String>? = null
var pathIsDirectory: Boolean? = null
for (it in annotations) { for (it in annotations) {
type = ParameterType.forParameterAnnotationClass(it) type = ParameterType.forParameterAnnotationClass(it)
@@ -95,6 +99,13 @@ fun Any.listParameters(): List<Parameter> {
label = it.label label = it.label
order = it.order order = it.order
} }
is PathParameter -> {
label = it.label
absolutePath = it.absolute
pathContext = it.context
pathExtensions = it.extensions
pathIsDirectory = it.directory
}
} }
} }
Parameter( Parameter(
@@ -109,7 +120,12 @@ fun Any.listParameters(): List<Parameter> {
precision = precision, precision = precision,
showVector = showVector, showVector = showVector,
invertY = invertY, invertY = invertY,
order = order absolutePath = absolutePath,
pathContext = pathContext,
pathExtensions = pathExtensions,
pathIsDirectory = pathIsDirectory,
order = order,
) )
} + this::class.declaredMemberFunctions.filter { } + this::class.declaredMemberFunctions.filter {
it.findAnnotation<ActionParameter>() != null it.findAnnotation<ActionParameter>() != null
@@ -130,6 +146,10 @@ fun Any.listParameters(): List<Parameter> {
precision = null, precision = null,
showVector = null, showVector = null,
invertY = null, invertY = null,
absolutePath = null,
pathContext = null,
pathExtensions = null,
pathIsDirectory = null,
order = order order = order
) )
}).sortedBy { it.order } }).sortedBy { it.order }

View File

@@ -1,4 +1,5 @@
import org.amshove.kluent.* import org.amshove.kluent.*
import org.junit.jupiter.api.Assertions.assertEquals
import org.openrndr.color.ColorRGBa import org.openrndr.color.ColorRGBa
import org.openrndr.extra.parameters.* import org.openrndr.extra.parameters.*
import org.openrndr.math.Vector2 import org.openrndr.math.Vector2
@@ -46,6 +47,9 @@ val a = object {
@OptionParameter("an option parameter", order = 11) @OptionParameter("an option parameter", order = 11)
var o = ParameterType.Option var o = ParameterType.Option
@PathParameter("a path parameter", order = 12)
var p = "bla.png"
} }
object TestAnnotations : Spek({ object TestAnnotations : Spek({
@@ -124,6 +128,12 @@ object TestAnnotations : Spek({
list[11].property?.name `should be equal to` "o" list[11].property?.name `should be equal to` "o"
list[11].label `should be equal to` "an option parameter" list[11].label `should be equal to` "an option parameter"
assertEquals(list[12].parameterType, ParameterType.Path)
assertEquals(list[12].property?.name, "p")
assertEquals(list[12].label, "a path parameter")
assertEquals(list[12].absolutePath, false)
assertEquals(list[12].pathContext, "null")
} }
} }
}) })

View File

@@ -0,0 +1,3 @@
# orx-property-watchers
Tools for setting up property watcher based pipelines

View File

@@ -0,0 +1,34 @@
plugins {
org.openrndr.extra.convention.`kotlin-multiplatform`
}
kotlin {
jvm {
testRuns["test"].executionTask {
useJUnitPlatform {
includeEngines("spek2")
}
}
}
sourceSets {
@Suppress("UNUSED_VARIABLE")
val commonMain by getting {
dependencies {
implementation(libs.openrndr.application)
implementation(libs.openrndr.math)
implementation(libs.kotlin.reflect)
}
}
@Suppress("UNUSED_VARIABLE")
val jvmTest by getting {
dependencies {
implementation(libs.kluent)
implementation(libs.spek.dsl)
runtimeOnly(libs.spek.junit5)
runtimeOnly(libs.kotlin.reflect)
}
}
}
}

View File

@@ -0,0 +1,136 @@
package org.openrndr.extra.propertywatchers
import org.openrndr.events.Event
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty0
/**
* Property watcher delegate
* @see watchingProperty
*/
class PropertyWatcherDelegate<V, R>(
private val property: KProperty0<V>,
private val valueChangedEvent: Event<V>,
private val cleaner: ((R) -> Unit)? = null,
val function: (V) -> R
) {
private var watchValue: V? = null
private var value: R? = null
operator fun getValue(any: Any, property: KProperty<*>): R {
val ref = this.property.get()
if (watchValue != ref) {
watchValue = ref
value?.let {
cleaner?.invoke(it)
}
value = function(ref)
}
return value ?: error("no value?")
}
}
/**
* Property watcher delegate
* @see watchingProperties
*/
class PropertyWatcherDelegate2<V0, V1, R>(
private val toWatch0: KProperty0<V0>,
private val toWatch1: KProperty0<V1>,
private val cleaner: ((R) -> Unit)? = null,
private val function: (V0, V1) -> R
) {
private var watchValue0: V0? = null
private var watchValue1: V1? = null
private var value: R? = null
operator fun getValue(any: Any, property: KProperty<*>): R {
val ref0 = toWatch0.get()
val ref1 = toWatch1.get()
if (watchValue0 != ref0 || watchValue1 != ref1) {
watchValue0 = ref0
watchValue1 = ref1
value?.let {
cleaner?.invoke(it)
}
value = function(ref0, ref1)
}
return value ?: error("no value?")
}
}
/**
* Property watcher delegate
* @see watchingProperties
*/
class PropertyWatcherDelegate3<V0, V1, V2, R>(
private val toWatch0: KProperty0<V0>,
private val toWatch1: KProperty0<V1>,
private val toWatch2: KProperty0<V2>,
private val cleaner: ((R) -> Unit)? = null,
private val function: (V0, V1, V2) -> R
) {
private var watchValue0: V0? = null
private var watchValue1: V1? = null
private var watchValue2: V2? = null
private var value: R? = null
operator fun getValue(any: Any, property: KProperty<*>): R {
val ref0 = toWatch0.get()
val ref1 = toWatch1.get()
val ref2 = toWatch2.get()
if (watchValue0 != ref0 || watchValue1 != ref1 || watchValue2 != ref2) {
watchValue0 = ref0
watchValue1 = ref1
value?.let {
cleaner?.invoke(it)
}
value = function(ref0, ref1, ref2)
}
return value ?: error("no value?")
}
}
/**
* Delegate property value to a function for which the value of a single property is watched
* @param property the property for which to watch for value changes
* @param function a function that maps the property value to a new value
*/
fun <V, R> watchingProperty(
property: KProperty0<V>,
cleaner: ((R) -> Unit)? = null,
function: (value: V) -> R
): PropertyWatcherDelegate<V, R> {
return PropertyWatcherDelegate(property, Event("value-changed-${property.name}"), cleaner, function)
}
/**
* Delegate property value to a function for which the values of 2 properties are watched
* @param property0 the first property for which to watch for value changes
* @param property1 the second property which to watch for value changes
* @param function a function that maps the two property values to a new value
*/
fun <V0, V1, R> watchingProperties(
property0: KProperty0<V0>,
property1: KProperty0<V1>,
cleaner: ((R) -> Unit)?,
function: (value0: V0, value1: V1) -> R
): PropertyWatcherDelegate2<V0, V1, R> {
return PropertyWatcherDelegate2(property0, property1, cleaner, function)
}
/**
* Delegate property value to a function for which the values of 3 properties are watched
* @param property0 the first property for which to watch for value changes
* @param property1 the second property which to watch for value changes
* @param property2 the third property which to watch for value changes
* @param function a function that maps the three property values to a new value
*/
fun <V0, V1, V2, R> watchingProperties(
property0: KProperty0<V0>,
property1: KProperty0<V1>,
property2: KProperty0<V2>,
cleaner: ((R) -> Unit)? = null,
function: (value0: V0, value1: V1, value2: V2) -> R
): PropertyWatcherDelegate3<V0, V1, V2, R> {
return PropertyWatcherDelegate3(property0, property1, property2, cleaner, function)
}

View File

@@ -0,0 +1,33 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.grayscale
import org.openrndr.draw.tint
import org.openrndr.drawImage
import org.openrndr.extra.propertywatchers.watchingImagePath
import org.openrndr.extra.propertywatchers.watchingProperty
fun main() {
application {
program {
val state = object {
var path = "demo-data/images/image-001.png"
val image by watchingImagePath(::path) {
drawImage(it.width, it.height) {
drawer.drawStyle.colorMatrix = grayscale()
drawer.image(it)
}
}
val redImage by watchingProperty(::image, cleaner = { it.destroy() }) {
drawImage(it.width, it.height) {
drawer.drawStyle.colorMatrix = tint(ColorRGBa.RED)
drawer.image(it)
}
}
}
extend {
drawer.image(state.redImage)
}
}
}
}

View File

@@ -0,0 +1,22 @@
import org.openrndr.application
import org.openrndr.extra.propertywatchers.watchingProperty
fun main() {
application {
program {
val state = object {
val x by watchingProperty(mouse::position) {
it.x
}
val xx by watchingProperty(::x) {
it * it
}
}
extend {
state.x
}
}
}
}

View File

@@ -0,0 +1,20 @@
package org.openrndr.extra.propertywatchers
import org.openrndr.Program
import org.openrndr.draw.ColorBuffer
import org.openrndr.draw.loadImage
import java.io.File
import kotlin.reflect.KProperty0
fun Program.watchingImagePath(pathProperty: KProperty0<String>, imageTransform: (ColorBuffer) -> ColorBuffer = { it }) =
watchingProperty(pathProperty, cleaner = { it.destroy() }) {
val file = File(it)
require(file.exists()) { "$it does not exist" }
require(file.isFile) { "$it is not a file" }
val image = loadImage(file)
val transformedImage = imageTransform(image)
if (image != transformedImage) {
image.destroy()
}
transformedImage
}

View File

@@ -51,6 +51,7 @@ include(
"orx-jvm:orx-olive", "orx-jvm:orx-olive",
"orx-jvm:orx-osc", "orx-jvm:orx-osc",
"orx-palette", "orx-palette",
"orx-property-watchers",
"orx-jvm:orx-panel", "orx-jvm:orx-panel",
"orx-jvm:orx-poisson-fill", "orx-jvm:orx-poisson-fill",
"orx-quadtree", "orx-quadtree",