diff --git a/build.gradle b/build.gradle index 40ffdecc..41c4e2ac 100644 --- a/build.gradle +++ b/build.gradle @@ -81,7 +81,7 @@ allprojects { compile "org.openrndr:openrndr-shape:$openrndrVersion" compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-core', version: '1.3.2' testImplementation "org.spekframework.spek2:spek-dsl-jvm:$spekVersion" - testImplementation "org.amshove.kluent:kluent:1.53" + testImplementation "org.amshove.kluent:kluent:1.59" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlinVersion" testRuntimeOnly "org.spekframework.spek2:spek-runner-junit5:$spekVersion" testRuntimeOnly "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" diff --git a/orx-fx/build.gradle b/orx-fx/build.gradle new file mode 100644 index 00000000..fff268b1 --- /dev/null +++ b/orx-fx/build.gradle @@ -0,0 +1,3 @@ +dependencies { + compile project(":orx-parameters") +} \ No newline at end of file diff --git a/orx-parameters/README.md b/orx-parameters/README.md new file mode 100644 index 00000000..028573c2 --- /dev/null +++ b/orx-parameters/README.md @@ -0,0 +1,41 @@ +# orx-parameters + +A collection of annotations and tools that are used to turn Kotlin properties into introspectable parameters. Parameters +are highly suitable for automatically generating user interfaces, but note that this is _not_ what `orx-parameters` does. + +Currently orx-parameters supplies the following annotations: + + - `DoubleParameter` + - `IntParameter` + - `DoubleParameter` + +## Annotation application + +Annotations can be applied to a properties inside a class or object class. + +````kotlin +val foo = object { + @DoubleParameter("a double scalar", 0.0, 1.0, order = 0) + var d = 1.0 + + @IntParameter("an integer scalar", 1, 100, order = 1) + var i = 1 + + @BooleanParameter("a boolean parameter", order = 2) + var b = false +} +```` + +## Querying parameters + +Given an instance of an annotated class we can list the parameters using the extension method +`Any.listParameters()` of our previously declared object `foo` + +```kotlin + import org.openrndr.extra.parameters.listParameters + + // .. + + val parameters = foo.listParameters() +``` + diff --git a/orx-parameters/build.gradle b/orx-parameters/build.gradle new file mode 100644 index 00000000..82634771 --- /dev/null +++ b/orx-parameters/build.gradle @@ -0,0 +1,3 @@ +dependencies { + implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion" +} \ No newline at end of file diff --git a/orx-parameters/src/main/kotlin/Annotations.kt b/orx-parameters/src/main/kotlin/Annotations.kt new file mode 100644 index 00000000..5f6d8601 --- /dev/null +++ b/orx-parameters/src/main/kotlin/Annotations.kt @@ -0,0 +1,102 @@ +package org.openrndr.extra.parameters + +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.KVisibility +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.findAnnotation + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class Description(val description: String) + +/** + * DoubleParameter annotation for a double precision Filter parameter + * @property label a short description of the parameter + * @property low the lowest value this parameter should be assigned + * @property high the highest value this parameter should be assigned + * @property precision a hint for precision in user interfaces + * @property order hint for where to place the parameter in user interfaces + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class DoubleParameter(val label: String, val low: Double, val high: Double, val precision: Int = 3, val order: Int = Integer.MAX_VALUE) + +/** + * IntParameter annotation for an integer Filter parameter + * @property label a short description of the parameter + * @property low the lowest value this parameter should be assigned + * @property high the highest value this parameter should be assigned + * @property order hint for where to place the parameter in user interfaces + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class IntParameter(val label: String, val low: Int, val high: Int, val order: Int = Integer.MAX_VALUE) + +/** + * BooleanParameter annotation for an integer Filter parameter + * @property label a short description of the parameter + * @property order hint for where to place the parameter in user interfaces + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +annotation class BooleanParameter(val label: String, val order: Int = Integer.MAX_VALUE) + +/** + * Parameter summary class. This is used by [listParameters] as a way to report parameters in + * a unified form. + * @property property the property that received any of the parameter annotations + * [BooleanParameter], [IntParameter], or [DoubleParameter] + * @property label a label that describes the property + * @property doubleRange a floating point based range in case [DoubleParameter] is used + * @property intRange an integer range in case [IntParameter] is used + * @property precision a precision hint in case a [DoubleParameter] annotation is used + * @property order a hint for where in the ui this parameter is placed, lower value means higher priority + */ +class Parameter( + val property: KMutableProperty1, + val label: String, + val doubleRange: ClosedRange?, + val intRange: IntRange?, + val precision: Int?, + val order: Int) + +/** + * List all parameters, (public var properties with a parameter annotation) + */ +fun Any.listParameters(): List { + return this::class.declaredMemberProperties.filter { + !it.isConst && + it.visibility == KVisibility.PUBLIC && + (it.findAnnotation() != null || + it.findAnnotation() != null || + it.findAnnotation() != null) + }.map { + val annotations = listOf(it.findAnnotation(), it.findAnnotation(), it.findAnnotation()).filterNotNull() + var intRange: IntRange? = null + var doubleRange: ClosedRange? = null + var order: Int = Integer.MAX_VALUE + var label: String = "" + var precision: Int? = null + + annotations.forEach { + when(it) { + is BooleanParameter -> { + label = it.label + order = it.order + } + is DoubleParameter -> { + label = it.label + doubleRange = it.low .. it.high + precision = it.precision + order = it.order + } + is IntParameter -> { + label = it.label + intRange = it.low .. it.high + order = it.order + } + } + } + Parameter(it as KMutableProperty1, label, doubleRange, intRange, precision, order) + }.sortedBy { it.order } +} \ No newline at end of file diff --git a/orx-parameters/src/test/kotlin/TestAnnotations.kt b/orx-parameters/src/test/kotlin/TestAnnotations.kt new file mode 100644 index 00000000..f30c69f2 --- /dev/null +++ b/orx-parameters/src/test/kotlin/TestAnnotations.kt @@ -0,0 +1,46 @@ +import org.amshove.kluent.`should be equal to` +import org.amshove.kluent.shouldBeInRange +import org.openrndr.extra.parameters.BooleanParameter +import org.openrndr.extra.parameters.DoubleParameter +import org.openrndr.extra.parameters.IntParameter +import org.openrndr.extra.parameters.listParameters +import org.spekframework.spek2.Spek +import org.spekframework.spek2.style.specification.describe + +val a = object { + @DoubleParameter("a double scalar", 0.0, 1.0, order = 0) + var d = 1.0 + + @IntParameter("an integer scalar", 1, 100, order = 1) + var i = 1 + + @BooleanParameter("a boolean parameter", order = 2) + var b = false +} + +object TestAnnotations : Spek({ + describe("an annotated object") { + it("has listable parameters") { + val list = a.listParameters() + list.size `should be equal to` 3 + list[0].property.name `should be equal to` "d" + list[0].label `should be equal to` "a double scalar" + list[0].doubleRange?.let { + it.start shouldBeInRange 0.0 .. 0.0001 + it.endInclusive shouldBeInRange 0.999 .. 1.001 + } + list[0].precision `should be equal to` 3 + + list[1].property.name `should be equal to` "i" + list[1].label `should be equal to` "an integer scalar" + list[1].intRange?.let { + it.start `should be equal to` 1 + it.endInclusive `should be equal to` 100 + } + + list[2].property.name `should be equal to` "b" + list[2].label `should be equal to` "a boolean parameter" + list[2].precision `should be equal to` null + } + } +}) diff --git a/settings.gradle b/settings.gradle index b9479ac0..5ee09606 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,6 +4,7 @@ include 'orx-camera', 'orx-compositor', 'orx-easing', 'orx-file-watcher', + 'orx-parameters', 'orx-filter-extension', 'orx-fx', 'orx-gradient-descent',