[orx-parameters, orx-property-watchers, orx-file-watcher, orx-gui] Add @PathParameter, file watcher delegates and property delegates
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
|
|||||||
28
orx-jvm/orx-gui/src/demo/kotlin/DemoPath01.kt
Normal file
28
orx-jvm/orx-gui/src/demo/kotlin/DemoPath01.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
3
orx-property-watchers/README.md
Normal file
3
orx-property-watchers/README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# orx-property-watchers
|
||||||
|
|
||||||
|
Tools for setting up property watcher based pipelines
|
||||||
34
orx-property-watchers/build.gradle.kts
Normal file
34
orx-property-watchers/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
orx-property-watchers/src/jvmMain/kotlin/ImagePath.kt
Normal file
20
orx-property-watchers/src/jvmMain/kotlin/ImagePath.kt
Normal 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
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user